License类商品接入示例工程
引导式阅读
Java
License类商品接入示例工程
作者
C***
上架时间
2023-11-14 01:33:57

1. 介绍

> 基于Spring boot构建,可以直接启动,通过修改application.properties配置文件里的server.port可以改变启动端口号, 默认访问路径为:http://127.0.0.1:8080/isv/produce?activity=getLicense&xxx ,可以通过卖家中心的应用工具菜单下的交付接口调试页面生成链接用来本地调试

新购商品

  • 1.客户购买商品并付款成功,云商店将调用本接口通知服务商客户购买商品的信息和客户信息,服务商收到该请求信息,需执行新购对应动作并将执行结果返回给云商店。

  • 2.服务商需要返回此订单对应的唯一实例ID(instanceId),对不同的新购订单实例ID(instanceId)不可一致。建议此ID直接使用该订单首次请求时云商店传入的businessId,以确保instanceId的唯一性。

    针对新购场景接口调用失败的情况,云商店会持续调用3小时(每小时1次)后停止调用,期间服务商可在“卖家中心> 生产接口消息”页面单击对应订单右侧操作栏的“重新启动”进行手动重试。若服务商接口问题在3小时调用时间内解决,则在下一次调用接口响应成功,订单开通成功;若服务商接口问题在3小时调用后仍未解决,系统则判断订单为失败,并自动进行订单退订。若商品单月因接口失败导致的失败订单超过5个,云商店将对该商品下架处理。

  • 3.服务商的服务器在处理接口请求时,需要做好幂等性处理。

    云商店服务有可能重发请求,针对同一订单号(orderId),服务商的服务器应当返回成功的响应及应用实例信息,不应该创建新的License实例,返回之前成功创建的实例信息即可。

商品续费

按周期售卖的商品,服务商必须实现续费接口生产开发。

  • 1.客户续费下单或试用商品转正后,云商店将调用该接口请求服务商执行业务延期,服务商接口需要执行将到期日进行更新,并返回通知云商店。

  • 2.服务商需要保障续费接口通畅,将业务进行延期,如续费失败,将可能导致用户的业务被释放的风险。

  • 3.针对续费场景接口调用失败的情况,云商店会调用1次,“卖家中心>生产接口消息”中可查询接口“异常信息”,请在修复接口异常后,通知云商店partner@huaweicloud.com对该失败调用进行重新调用或自行在“卖家中心> 生产接口消息”页面中查询接口“异常信息”,单击右侧操作栏的“重新启动”进行手动重试。

商品过期

  • 1.客户购买的商品过期时,云商店调用该接口,服务商接到该通知以后应该将指定的实例冻结。

  • 2.针对过期场景接口调用失败的情况,云商店会调用1次,“卖家中心>生产接口消息”中可查询接口“异常信息”,请在修复接口异常后,通知云商店partner@huaweicloud.com对该失败调用进行重新调用或自行在“卖家中心> 生产接口消息”页面中查询接口“异常信息”,单击右侧操作栏的“重新启动”进行手动重试。

商品资源释放

  • 1.云商店将在释放客户购买的商品时调用该接口,服务商接收到该通知以后可以删除指定的实例。

  • 2.当客户已购买的商品到期不续订且超过保留期、或申请退订商品成功时,将释放客户购买的商品资源。

  • 3.针对释放场景接口调用失败的情况,云商店会调用1次,“卖家中心>生产接口消息”中可查询接口“异常信息”,请在修复接口异常后,通知云商店partner@huaweicloud.com对该失败调用进行重新调用或自行在“卖家中心> 生产接口消息”页面中查询接口“异常信息”,单击右侧操作栏的“重新启动”进行手动重试。

2. 开发时序图/流程图

img.png

流程说明如下:

  • 申请入驻云商店,成为服务商。

  • 云商店运营人员审核公司的资质信息。

  • 加入联营计划,成为联营服务商。

  • 准备生产接口服务器,根据本接入指南开发生产接口。

  • 在卖家中心调试生产接口。

  • 在卖家中心申请发布商品。

  • 云商店运营人员审批通过后,产品为联营商品。

  • 在卖家中心自助管理生产接口通知消息

3. 前置条件

3.1 获取华为云账号对应的Access Key(AK)。

  • 请在卖家中心“服务商管理 > 服务商信息”页面上查看您的AK。具体请参见 获取Key值

3.2 运行环境

  • Java JDK 1.8 及其以上版本。

3.3 SDK

  • 不涉及SDK。

4. 关键代码片段

package com.examples.controller; import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; import java.io.IOException; import java.util.Map; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.examples.model.BaseResp; import com.examples.model.ExpireLicenseReq; import com.examples.model.GetLicenseInfo; import com.examples.model.GetLicenseReq; import com.examples.model.GetLicenseResp; import com.examples.model.RefreshLicenseReq; import com.examples.model.ReleaseLicenseReq; import com.examples.service.IsvProduceService; import com.examples.util.IsvProduceAPI; import com.examples.util.ParamConverter; import com.examples.util.ResultCodeEnum; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * License类商品接入示例工程,具体接入流程参考:<a href="https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649152.html">链接</a> */ @RestController @RequestMapping(value = "isv/produce") public class IsvProduceController { ObjectMapper mapper = new ObjectMapper(); /** * 云市场分配的秘钥,在服务商入驻云市场成功后,isvConsole可以查看,因为安全性,并不建议写死 */ // 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全; // 本示例以ak和sk保存在环境变量中来实现身份认证为例,运行示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。 private static final String ACCESS_KEY = System.getenv("HUAWEICLOUD_SDK_AK");; /** * 返回体header中的签名key */ private static final String BODY_SIGN_KEY = "Body-Sign"; private static final String CONTENT_TYPE = "Content-Type"; /** * 返回体header中签名key的value */ private static final String BODY_SIGN_VALUE = "sign_type=\"HMAC-SHA256\", signature= \"{signature}\""; @Resource private IsvProduceService isvProduceService; /** * 新购生产接口请求处理 * 对接指导:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0000001353181669.html * * @param request 请求体 * @param response 返回 * @throws IOException 异常 */ @RequestMapping(value = "", params = "activity=getLicense") public void getLicense(HttpServletRequest request, HttpServletResponse response) throws IOException { Map<String, String[]> paramsMap = request.getParameterMap(); if (verifyParams(IsvProduceAPI.verificationRequestQueryParams(paramsMap, ACCESS_KEY), response)) { return; } // 将请求参数封装到对象中 GetLicenseReq getLicenseReq = ParamConverter.convertGetLicenseReq(paramsMap); // activity=getLicense时,扩展参数必填,需要进行解密 getLicenseReq.setSaasExtendParams(IsvProduceAPI.decryptSaasExtendParams(getLicenseReq.getSaasExtendParams())); // 服务商实现具体的新购资源生成逻辑 GetLicenseInfo getLicenseInfo = isvProduceService.getLicense(getLicenseReq); if (null == getLicenseInfo) { writeErrorResp(ResultCodeEnum.OTHER_INNER_ERROR, response); return; } // 生成资源成功后,替换以下变量值为实际业务参数 构造返回 String instanceId = getLicenseInfo.getInstanceId(); // 资源实例id,最好使用uuid,确保instanceId的唯一性 String license = getLicenseInfo.getLicense(); // license识别码 String responseBody = buildGetLicenseSuccessResponse(instanceId, license); writeSuccessResp(response, responseBody); } /** * 续费生产接口请求处理 * 对接指导:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0000001353381237.html * * @param request 请求体 * @param response 返回 * @throws IOException 异常 */ @RequestMapping(value = "", params = "activity=refreshLicense") public void refreshLicense(HttpServletRequest request, HttpServletResponse response) throws IOException { Map<String, String[]> paramsMap = request.getParameterMap(); // 校验参数是否合法,只做最基础的合法性校验 if (verifyParams(IsvProduceAPI.verificationRequestQueryParams(paramsMap, ACCESS_KEY), response)) { return; } // 将请求参数封装到对象中,后续业务可以直接使用 RefreshLicenseReq refreshLicenseReq = ParamConverter.convertRefreshLicenseReq(paramsMap); // 服务商实现具体的资源续费逻辑 boolean isSuccess = isvProduceService.refreshLicense(refreshLicenseReq); if (isSuccess) { writeSuccessResp(response, null); return; } writeErrorResp(ResultCodeEnum.OTHER_INNER_ERROR, response); } /** * 过期生产接口请求处理 * 对接指导:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0000001300621648.html * * @param request 请求体 * @param response 返回 * @throws IOException 异常 */ @RequestMapping(value = "", params = "activity=expireLicense") public void expireLicense(HttpServletRequest request, HttpServletResponse response) throws IOException { Map<String, String[]> paramsMap = request.getParameterMap(); // 校验参数是否合法,只做最基础的合法性校验 if (verifyParams(IsvProduceAPI.verificationRequestQueryParams(paramsMap, ACCESS_KEY), response)) { return; } // 将请求参数封装到对象中,后续业务可以直接使用 ExpireLicenseReq expireLicenseReq = ParamConverter.convertExpireLicenseReq(paramsMap); // 服务商实现具体的资源过期逻辑 boolean isSuccess = isvProduceService.expireLicense(expireLicenseReq); if (isSuccess) { // 过期处理不需要返回其他数据,只要返回成功结果码 writeSuccessResp(response, null); return; } writeErrorResp(ResultCodeEnum.OTHER_INNER_ERROR, response); } /** * 资源释放生产接口请求处理 * 对接指导:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0000001353261785.html * * @param request 请求体 * @param response 返回 * @throws IOException 异常 */ @RequestMapping(value = "", params = "activity=releaseLicense") public void releaseLicense(HttpServletRequest request, HttpServletResponse response) throws IOException { Map<String, String[]> paramsMap = request.getParameterMap(); // 校验参数是否合法,只做最基础的合法性校验 if (verifyParams(IsvProduceAPI.verificationRequestQueryParams(paramsMap, ACCESS_KEY), response)) { return; } // 将请求参数封装到对象中,后续业务可以直接使用 ReleaseLicenseReq releaseLicenseReq = ParamConverter.convertReleaseLicenseReq(paramsMap); // 服务商实现具体的资源释放逻辑 boolean isSuccess = isvProduceService.releaseLicense(releaseLicenseReq); if (isSuccess) { // 资源释放不需要返回其他数据,只要返回成功结果码 writeSuccessResp(response, null); return; } writeErrorResp(ResultCodeEnum.OTHER_INNER_ERROR, response); } /** * 输出错误响应 * * @param resultCodeEnum 错误响应内容 * @param response HttpServletResponse * @throws IOException 异常 */ private void writeErrorResp(ResultCodeEnum resultCodeEnum, HttpServletResponse response) throws IOException { String responseBody = buildFailedResponse(resultCodeEnum.getResultCode(), resultCodeEnum.getResultMsg()); // 对待返回的请求体计算签名 String signature = IsvProduceAPI.generateResponseBodySignature(ACCESS_KEY, responseBody); // 将签名放到header中 response.setHeader(BODY_SIGN_KEY, BODY_SIGN_VALUE.replace("{signature}", signature)); // 资源生成失败 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().print(responseBody); } /** * 根据参数验证结果做不同的响应 * * @param verificationResult 参数校验结果 * @param response HttpServletResponse * @return true:参数验证不通过,false:参数验证通过 * @throws IOException 异常 */ private boolean verifyParams(boolean verificationResult, HttpServletResponse response) throws IOException { // 校验参数是否合法,只做最基础的合法性校验 if (!verificationResult) { writeErrorResp(ResultCodeEnum.INVALID_PARAM, response); return true; } return false; } /** * 输出正确响应结果 * * @param response HttpServletResponse * @param successResp 成功响应内容 * @throws IOException 异常 */ private void writeSuccessResp(HttpServletResponse response, String successResp) throws IOException { String responseBody; if (StringUtils.isBlank(successResp)) { // 返回成功结果码 responseBody = buildSuccessResponse(); } else { responseBody = successResp; } // 对待返回的请求体计算签名 String signature = IsvProduceAPI.generateResponseBodySignature(ACCESS_KEY, responseBody); // 将签名放到header中 response.setHeader(BODY_SIGN_KEY, BODY_SIGN_VALUE.replace("{signature}", signature)); response.setHeader(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE); response.getWriter().print(responseBody); } /** * 构建失败响应体 * 具体可用错误码参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649057.html * * @param code 返回码 * @param message 错误信息 * @return 构造好的失败返回体 */ private String buildFailedResponse(String code, String message) throws JsonProcessingException { BaseResp result = new BaseResp(); result.setResultMsg(message); result.setResultCode(code); return mapper.writeValueAsString(result); } private String buildGetLicenseSuccessResponse(String instanceId, String license) throws JsonProcessingException { mapper.getFactory().configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true); GetLicenseResp getLicenseResp = new GetLicenseResp(); getLicenseResp.setResultMsg(ResultCodeEnum.SUCCESS.getResultMsg()); getLicenseResp.setResultCode(ResultCodeEnum.SUCCESS.getResultCode()); getLicenseResp.setInstanceId(instanceId); getLicenseResp.setLicense(license); return mapper.writeValueAsString(getLicenseResp); } /** * 构建续费,过期,资源释放成功响应体 * * @return 构建好的成功返回体 */ private String buildSuccessResponse() throws JsonProcessingException { BaseResp result = new BaseResp(); result.setResultMsg(ResultCodeEnum.SUCCESS.getResultMsg()); result.setResultCode(ResultCodeEnum.SUCCESS.getResultCode()); return mapper.writeValueAsString(result); } }

5. 返回结果示例

  • 新购商品
{ "resultCode": "000000", "resultMsg": "success.", "instanceId": "6K1zfDrIUQxWyXDrYLV6fsHlLmvW9sBB", "license": "XsSWv1KOcTWuV7EOaaHdeE92oWmp1Q9u" }
  • 商品续费
{ "resultCode": "000000", "resultMsg": "success." }
  • 商品过期
{ "resultCode": "000000", "resultMsg": "success." }
  • 商品资源释放
{ "resultCode": "000000", "resultMsg": "success." }

6. 参考链接

  • 联营License类商品接入流程参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649152.html

  • 联营License类商品接口功能及说明参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649153.html

  • 联营License类商品接入准备工作参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649154.html

  • 联营License类商品接口描述参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0000001300142204.html

  • 联营License类商品接口调试参考:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649158.html