按需使用量推送(新)
接口说明
用户在云商店开通按需资源并产生使用量后,商家需通过此接口返回用户的实际使用量话单,云商店获取话单后根据使用量计费并对用户扣费。
URI
POST https://mkt.myhuaweicloud.com/api/mkp-openapi-public/global/v1/isv/usage-data
“mkt.myhuaweicloud.com”域名不可用时请使用“mkt.myhuaweicloud.cn”进行重试。
① 按需计量商品购买和使用流程
② 用完即停套餐包商品购买和使用流程
请求消息
请求参数说明请参见下表:
请求方法:POST
参数 |
是否必选 |
类型 |
最大字符长度 |
说明 |
---|---|---|---|---|
signature |
是 |
String |
1000 |
@Header 接口签名(base64(hmacSHA256(ISV对接秘钥,ts={ts}&nonce={nonce}&body={body}))) 此处body按自然排序后参与签名 |
ts |
是 |
String |
20 |
@Header 接口请求时的unix时间戳(毫秒数) |
nonce |
是 |
String |
64 |
@Header 安全随机数 |
usage_records |
是 |
List<UsagePushData> |
1000 |
业务使用量纪录集合,UsagePushData个数不得超过1000。 |
参数 |
是否必选 |
参数类型 |
最大字符长度 |
描述 |
---|---|---|---|---|
instance_id |
是 |
String |
64 |
按需实例ID(注意:使用按需接口返回的) |
record_time |
是 |
String |
20 |
使用量记录生成时间(UTC),格式为: yyyyMMdd'T'HHmmss'Z' |
begin_time |
是 |
String |
20 |
计量开始时间(UTC),格式为: yyyyMMdd'T'HHmmss'Z' |
end_time |
是 |
String |
20 |
计量结束时间(UTC),格式为: yyyyMMdd'T'HHmmss'Z' |
usage_value |
是 |
String |
17 |
使用量具体值,最多支持4位有效小数,需为大于0的数值 |
metering_sn |
是 |
String |
64 |
ISV话单记录唯一性标识,建议是随机码 |
relate_pkg_instance |
否 |
String |
64 |
用完即停套餐包场景下话单必传,需要传递当前用量对应的扣减包实例ID |
请求消息示例:
public class PushDemoForPublic_2_0 { static final String ALGO = "HmacSHA256"; static String FORMAT = "ts=%s&nonce=%s&body=%s"; static String ISV_ACCESS_KEY = "xxx"; //商家秘钥-已绑定基础接口的秘钥值 public static void main(String[] args) throws IOException { List<UsagePushData2> pushDataList = new ArrayList<>(); UsagePushData2 usagePushData = new UsagePushData2(); //实例id usagePushData.setInstance_id("instanceid"); //时间格式化pattern DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyyMMdd'T'HHmmss'Z'"); DateTime now = DateTime.now(); //时间自定义:开始时间 usagePushData.setBegin_time(now.withTimeAtStartOfDay().toString(formatter)); //时间自定义:结束时间 usagePushData.setEnd_time(now.plusDays(1).withTimeAtStartOfDay().toString(formatter)); //时间自定义:统计时间 usagePushData.setRecord_time(usagePushData.getEnd_time()); //统计用量 usagePushData.setUsage_value("1.1"); // 话单唯一标识 usagePushData.setMetering_sn(UUID.randomUUID().toString()); pushDataList.add(usagePushData); JSONObject requestParam = new JSONObject(); requestParam.put("usage_records", JSONArray.parseArray(JSON.toJSONString(pushDataList))); String url = "https://mkt.myhuaweicloud.com/api/mkp-openapi-public/global/v1/isv/usage-data"; Request request = new Request(); request.setKey("xxx"); request.setSecret("xxx"); // 我的凭证-申请访问秘钥 request.setMethod("POST"); request.setUrl(url); request.addHeader("Host", "mkt.myhuaweicloud.com"); // 时间戳 String ts = System.currentTimeMillis() + ""; // nonce 只被使用一次的任意或非重复的随机数值。 String nonce = "number used once"; // 生成签名 String signature = genSignature(requestParam, nonce, ts); request.addHeader("ts", ts); request.addHeader("nonce", nonce); request.addHeader("signature", signature); request.addHeader("Content-Type", "application/json"); request.setBody(requestParam.toJSONString()); CloseableHttpClient client = null; try{ HttpRequestBase signedRequest = Client.sign(request, Constant.SIGNATURE_ALGORITHM_SDK_HMAC_SHA256); client = (CloseableHttpClient) SSLCipherSuiteUtil.createHttpClient(Constant.INTERNATIONAL_PROTOCOL); HttpResponse response = client.execute(signedRequest); HttpEntity resEntity = response.getEntity(); if (resEntity != null) { String str = EntityUtils.toString(resEntity, "UTF-8"); System.out.println(str); } }catch (Exception e){ System.out.println("report usage data failed"); }finally { if(client!=null){ client.close(); } } } private static String toSnakeString(Object o) { Map<String, Object> jsonToMap = JsonUtils.decode(JsonUtils.encode(o), new TypeReference<Map<String, Object>>() { }); Map<String, Object> toSortedMap = jsonToMap.entrySet() .stream() .collect(Collectors.toMap(e -> e.getKey(), entry -> { if (entry.getValue() instanceof List) { return ((List<?>) entry.getValue()).stream() .map(e -> JsonUtils.decode(JsonUtils.encode(e), new TypeReference<TreeMap<String, Object>>() { })) .collect(Collectors.toList()); } return entry.getValue(); }, (u, v) -> v)); return JsonUtils.encode(new TreeMap<>(toSortedMap)); } /** * 生成签名接口 * * @param requestParam 请求提 * @param nonce 随机值 * @param timestamp 时间戳 * @return 签名 */ private static String genSignature(JSONObject requestParam, String nonce, String timestamp) { try { String originSignData = String.format(FORMAT, timestamp, nonce, toSnakeString(requestParam)); Mac mac; mac = Mac.getInstance(ALGO); mac.init(new SecretKeySpec(ISV_ACCESS_KEY.getBytes(StandardCharsets.UTF_8), ALGO)); byte[] signatureBytes = mac.doFinal(originSignData.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signatureBytes); } catch (NoSuchAlgorithmException | InvalidKeyException e) { log.error("check signature failed for hmac hash", e); } return null; } }
1、接口调用上传时,如果话单数据异常,不会在接口层进行报错;后台定时任务会对上传的数据进行校验和处理,生成可以使用的话单数据。如果后台数据处理失败,需要商家重新进行上报。
异常数据可以在 “卖家中心>交易管理>话单管理” 进行查看!
2、话单上报周期要求:
- 按小时计费
需要至少每小时上报一次话单数据,且最好在消费时间下一个小时的0-15分钟内完成上报,譬如,用户消费时间在13:25,最好在14:00-14:15内上报,这样能及时给用户扣费,否则就会出现扣费延迟,如果无法实现实时上报,需要在2小时完成上报
- 按天计费
推荐每小时将已经明确的用量话单上报到云商店,如果必须每天汇总上报一次,需要保证必须在次日的00:00-00:15内完成上报,最大不能超过01:00,否则用户的扣费会延迟到下一天
3、话单数据上报要求:
- 资源未关闭:
- 话单开始时间(begin_time)≥资源开始时间
- 话单开始时间(begin_time)≤ 话单结束时间(end_time)≤ 话单上报时间
- 资源关闭后:
- 话单结束时间(end_time)≤ 资源关闭时间
4、话单上报中的时间为UTC 时间,与北京时间相差 8 小时;
5、同一个记录的时间,即begin_time 和end_time 时间都一致的情况下,如多次上报记录,会被识别为重复话单,只会处理第一条数据,一旦话单采集后形成正式账单将无法逆向错误话单(话单采集时间:按天计费为每天凌晨1点,按小时计费为每小时15分)。
重复话单将视为异常,异常数据可以在 “卖家中心>交易管理>话单管理” 进行查看!
6、使用量推送接口使用按需交易中生成的按需实例ID,不能使用套餐包返回的实例ID。
响应消息
响应参数说明请参见下表:
参数 |
是否必选 |
类型 |
最大字符长度 |
说明 |
---|---|---|---|---|
error_code |
M |
String |
6 |
调用结果码。 具体请参见调用结果码说明。 |
error_msg |
O |
String |
255 |
调用结果描述。 |
data |
O |
AbnormalUsageDataInfo |
返回异常话单错误信息 |
参数 |
是否必选 |
参数类型 |
最大字符长度 |
描述 |
---|---|---|---|---|
abnormal_usage_data |
是 |
List<AbnormalUsageData> |
1000 |
异常话单列表 |
参数 |
是否必选 |
参数类型 |
最大字符长度 |
描述 |
---|---|---|---|---|
metering_sn |
是 |
String |
64 |
ISV话单记录唯一性标识 |
error_code |
是 |
String |
16 |
话单级错误码 001:实例不存在; 002:时间格式异常; 003:用量异常; 004:缺少话单唯一标识; 005:话单唯一标识重复 006:实例对应的商品已退市 007:话单时间已过期 009:实例与ISV不匹配 010:话单重复 011:话单时间范围无效 012:实例非按需资源 013:实例资源状态异常 014:实例资源已关闭 015:话单开始时间小于资源开通时间 016:实例资源开通中 017:用完即停场景下,relate_pkg_instance为空 018:用完即停场景下,relate_pkg_instance不合法,实例不存在或与instance_id不匹配 |
error_msg |
是 |
String |
255 |
话单错误描述 |
错误码:
http状态码 |
error_code |
error_msg |
描述 |
---|---|---|---|
200 |
MKT.0000 |
Success |
请求成功 |
500 |
94060001 |
System error! |
其它服务内部错误 |
401 |
94060002 |
Auth failed! |
输入参数校验失败 参数范围超限,非法值或格式错误 |
400 |
94060004 |
Param invalid |
参数无效 输入非接口定义的参数,多参数或少必选参数 比如:传递非法数值、没有实例id等 |
400 |
94060005 |
Time format error |
时间格式错误 |
400 |
94060006 |
TimeStamp invalid |
时间戳无效 |
401 |
94060007 |
Signature invalid |
签名校验失败 |
400 |
94060008 |
Replay error |
请求重放错误 |
500 |
94060009 |
Failed to report usage data |
话单调用失败 |
401 |
94060010 |
Isv status invalid |
ISV状态无效 |
200 |
94060999 |
Failed |
该错误码时,会返回话单级错误信息,详见响应示例 |
当您调用API时,如果遇到“APIGW”开头的错误码,请参见API网关错误码进行处理。
响应消息示例:
{ "error_code":"MKT.0000", "error_msg":"Success" } { "error_code": "94060999", "error_msg": "Failed", "data": { "abnormal_usage_data": [ { "error_code": "005", "error_msg": "METERING_SN_DUPLICATE", "metering_sn": "6c75c177b5fe4b8cbb6fc2aa33facfcb" } ] } }