API对接实战

目录

一 背景

二 了解B公司接口的基础约定

三 基础域名

四 请求及相应格式说明

五 确定要对接哪些API

六 根据API文档,编写一些基础工具类。

七 根据API文档,编写必要的DTO

八 针对每个API方法,进行对接

九 对接代码结构

十 一些对接技巧


一 背景

在平时工作中,经常会遇到的一种场景是:A公司要对接B公司的API方法,这时,A公司就要阅读B公司的接口文档,从接口文档中找到自己需要对接的API,并根据接口文档的要求,完成编码工作,最终完成对接工作。

本篇是站在A公司的角度,去对接B公司API接口的实战。

二 了解B公司接口的基础约定

一般情况下,B公司都会给出以下类似约定来满足基础对接,并且会提供测试环境和正式环境的两套信息。

appkey:A公司商户平台 id

appsecret:A公司商户平台 secret

三 基础域名

一般情况下,B公司会提供测试环境和生产环境两个基础域名。

例如

测试环境 : https://api-b-dev.com.cn

生产环境 : https://api-b-prod.com.cn

四 请求及相应格式说明

一般情况下,B公司会提供请求及相应的基础格式说明。

例如:

1 请求方式

post

2 请求消息格式

application/json

3 响应消息格式

application/json

4 请求公共参数

例如,B公司有以下要求

所有接口均需要以 Http Header 方式传递以下参数;

参数名

描述

必填

appkey

商户平台 id

request_id

请求标识 ,每次请求唯一

sign_type

签名方法,固定为 sha256

signature

签名,算法为 HMACSHA256(appkey+timestamp+appsecret,appsecret)

version

版本, 固定为 2.0

callback_url

回调地址, 以 https://或者 http://开头并进行 base64 编码

是(同步响应的接口可不必填写)

timestamp

时间戳(秒), 30 分钟过期

当然,不同的公司提供的参数各不相同,因公司而异。

5 响应/回调参数说明

例如:B公司所有API都有响应,并且有的API还有回调响应,不论是响应还是回调响应,它们的参数格式都一样。

参数

类型

描述

code

int

状态码

msg

String

消息

request_id

String

请求时的 request_id

data

Object

数据

appkey

String

商户平台id

6 针对异步回调的说明

例如:B公司对异步回调说明如下:

异步回调:

某些特定的接口需要异步返回结果,因此需商户A提供一个回调地址,将其进行base64 编码后,配置在 Http 请求 Header 中的 callback_url 里。

应答机制:

应答机制是指当商户A收到B公司数据通知时,必须回写 success 字符串,不区分大小写,B公司收到该“ success”,便认为商户A已收到通知; 否则会继续重复请求回调接口 3 次, 时间间隔为 1s, 5s, 30s。如果 4 次都访问不通,则会间隔 3h 继续轮询回调。

回调解密:

回调使用 aes 加密,需解密后使用。为避免由于网络波动造成回调失败,长时间未收到回调,请主动查询。

7 请求体加密说明及示例

数据采用AES加密,加密后作为data的值。

示例:

加密前:

{'settlement_code':['JS19BUB14F5D8D4C'],'random_code':['19BUB14F5D8D4C','19BUAD0E89D780']}

加密后:

{'data':'236agZcupcSsMZghtlmzhb7lEWzGZc3FO5GWQyrSB5kP/y1ESvd+CuBgQiWU/fwAICY/s0mideku/rXSKEb8In41F4SkUVLyLzYoYGed4QTjsqohTM0T6wmbkOiT1TH3'}

对 {'settlement_code':['JS19BUB14F5D8D4C'],'random_code':['19BUB14F5D8D4C','19BUAD0E89D780']} 进行AES加密,结果为:

236agZcupcSsMZghtlmzhb7lEWzGZc3FO5GWQyrSB5kP/y1ESvd+CuBgQiWU/fwAICY/s0mideku/rXSKEb8In41F4SkUVLyLzYoYGed4QTjsqohTM0T6wmbkOiT1TH3

8 回调解密说明以及示例

数据采用AES解密,解密data值部分,解密后是json字符串

解密前:

  1. {
  2. 'code':0,'
  3. 'msg':'处理成功',
  4. 'request_id':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78',
  5. 'data':'TI6H4Zx7YeWM0dSiial6L+nCvrEv8Oqk1ZFhWXqYZcRzzZyy/xECQW0nf
  6. DszpNDmRMlSmsWkBJMmu4a/PmBivUBoNJwFBzAnOfn8gtYKdxDU16lDFwN5d/I
  7. W1UJijJ2lU5YkDs/rMTyRN1NTR+0vJ1So0lmeZQiGQWEwE5t4wZykSC3cMQZyvJ95
  8. 2J7KU6aBXv1ZUGncZbWHQQaLw4UxFaBWIO8bVlkBIAqzolswI4dhtqBzFwmdEx+7
  9. hzHSeidOVbIja5adgKMAjvIUTdtUEb/cO0ipO6QbK8wglk6dQ8+7rFTchBYIoaaqM9Sf
  10. hcdvAYuSGk6yHIyN4GEtLBA5Zw==',
  11. 'appkey':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78'
  12. }

解密后:

  1. {
  2. 'code':0,'
  3. 'msg':'处理成功',
  4. 'request_id':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78',
  5. 'data':
  6. {
  7. 'settlement_code':'JS19BR19A690E9F9',
  8. 'order_random_code':'09708757-7ea1-4fda',
  9. 'refund_merchant_amount':54736.84,
  10. 'refund_service_amount':263.16,
  11. 'change_code':'FW19BRAA9A200255',
  12. 'change_merchant_amount':4263.16,
  13. 'change_service_amount':236.84
  14. },
  15. 'appkey':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78'
  16. }

9 一般公司B还会对code值进行说明。

五 确定要对接哪些API

一般情况下,公司B会针对某个项目提供必要的API,我们往往只需要对接少部分API接口,因此,首先确认要对接哪些API方法。我们只需要按照API的要求进行对接即可。

六 根据API文档,编写一些基础工具类。

工具类分两类。一类工具类,公司B会提供DEMO,我们拿来用即可。另外一类就需要自己根据API要求自己编写了。

1 公司B提供的基础工具类

例如,公司B提供了AES加解密以及签名的工具类

  1. import org.apache.commons.codec.binary.Base64;
  2. import javax.crypto.Cipher;
  3. import javax.crypto.Mac;
  4. import javax.crypto.spec.IvParameterSpec;
  5. import javax.crypto.spec.SecretKeySpec;
  6. import java.security.Security;
  7. /**
  8. * AES加解密工具
  9. */
  10. public class EncryptUtil {
  11. private static final String CipherMode='AES/CBC/PKCS7Padding';
  12. private static final String EncryptAlg ='AES';
  13. private static final String Encode='UTF-8';
  14. private static final String APPSECRET = '7da8046aa2da46bfb08429058e910081';
  15. private static final String AESIV = 'ff465fdecc764337';
  16. /**
  17. * 加密:有向量16位,结果转base64
  18. * @param context
  19. * @return
  20. */
  21. public static String encrypt(String context) {
  22. try { //下面这行在进行PKCS7Padding加密时必须加上,否则报错
  23. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
  24. byte[] content=context.getBytes(Encode);
  25. Cipher cipher = Cipher.getInstance(CipherMode);
  26. cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(APPSECRET.getBytes(Encode), EncryptAlg), new IvParameterSpec(AESIV.getBytes(Encode)));
  27. byte[] data = cipher.doFinal(content);
  28. String result= Base64.encodeBase64String(data);
  29. return result;
  30. } catch (Exception e) {
  31. e.printStackTrace();
  32. }
  33. return null;
  34. }
  35. /**
  36. * 解密
  37. * @param context
  38. * @return
  39. */
  40. public static String decrypt(String context) {
  41. try {
  42. byte[] data=Base64.decodeBase64(context);
  43. Cipher cipher = Cipher.getInstance(CipherMode);
  44. cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(APPSECRET.getBytes(Encode), EncryptAlg), new IvParameterSpec(AESIV.getBytes(Encode)));
  45. byte[] content = cipher.doFinal(data);
  46. String result=new String(content,Encode);
  47. return result;
  48. } catch (Exception e) {
  49. e.printStackTrace();
  50. }
  51. return null;
  52. }
  53. /**
  54. * 生成 HMACSHA256
  55. * @param data 待处理数据
  56. * @param key 密钥
  57. * @return 加密结果
  58. * @throws Exception
  59. */
  60. public static String HMACSHA256(String data, String key) throws Exception {
  61. Mac sha256_HMAC = Mac.getInstance('HmacSHA256');
  62. SecretKeySpec secret_key = new SecretKeySpec(key.getBytes('UTF-8'), 'HmacSHA256');
  63. sha256_HMAC.init(secret_key);
  64. byte[] array = sha256_HMAC.doFinal(data.getBytes('UTF-8'));
  65. StringBuilder sb = new StringBuilder();
  66. for (byte item : array) {
  67. sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
  68. }
  69. return sb.toString();
  70. }
  71. public static void main(String[] args) {
  72. //test256();
  73. testEncrypt();
  74. //testdecrypt();
  75. }
  76. private static void testdecrypt() {
  77. String s1 = 'TI6H4Zx7YeWM0dSiial6L+nCvrEv8Oqk1ZFhWXqYZcRzzZyy/xECQW0nf' +
  78. 'DszpNDmRMlSmsWkBJMmu4a/PmBivUBoNJwFBzAnOfn8gtYKdxDU16lDFwN5d/I' +
  79. 'W1UJijJ2lU5YkDs/rMTyRN1NTR+0vJ1So0lmeZQiGQWEwE5t4wZykSC3cMQZyvJ95' +
  80. '2J7KU6aBXv1ZUGncZbWHQQaLw4UxFaBWIO8bVlkBIAqzolswI4dhtqBzFwmdEx+7hzHSeid' +
  81. 'OVbIja5adgKMAjvIUTdtUEb/cO0ipO6QbK8wglk6dQ8+7rFTchBYIoaaqM9Sf' +
  82. 'hcdvAYuSGk6yHIyN4GEtLBA5Zw==';
  83. System.out.println(decrypt(s1));
  84. }
  85. private static void testEncrypt() {
  86. String s = '{\'name\':\'小明\',\'certificate_num\':\'451121196209260032\',\'certificate_type\':1,\'phone_num\':\'1388888888\',\'merchant_id\':\'c7c114d5da444df2b5d47a66c9c11111\'}';
  87. String afterEncrypt = encrypt(s);
  88. System.out.println(afterEncrypt);
  89. }
  90. private static void test256() {
  91. // 签名算法
  92. String s2 = 'c7c114d5da444df2b5d47a66c9cbd3fc16010271967da8046aa2da46bfb08429058e910081';
  93. String key = '7da8046aa2da46bfb08429058e911111';
  94. try {
  95. String s1 = HMACSHA256(s2,key);
  96. System.out.println(s1);
  97. } catch (Exception e) {
  98. e.printStackTrace();
  99. }
  100. }
  101. }

2 根据API要求自己编写的工具类

例如:根据公司B的要求,编写特定的post方法

  1. // 满足公司B的post方法
  2. public static String sendPostByJsonWithHeader(String url, String body, Map<String, String> headers) throws Exception {
  3. CloseableHttpClient httpclient = HttpClients.custom().build();
  4. HttpPost post = null;
  5. String resData = null;
  6. CloseableHttpResponse result = null;
  7. try {
  8. // 封装 url,并且是 post 请求。
  9. post = new HttpPost(url);
  10. HttpEntity entity = new StringEntity(body, Consts.UTF_8);
  11. // 基本配置
  12. post.setConfig(RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(30000).build());
  13. // 封装消息头
  14. if (null != headers && !headers.isEmpty()) {
  15. for (Entry<String, String> entry : headers.entrySet()) {
  16. post.setHeader(entry.getKey(), entry.getValue());
  17. }
  18. }
  19. // 消息头支持 json
  20. post.setHeader('Content-Type', 'application/json');
  21. // 封装数据
  22. post.setEntity(entity);
  23. // 发送请求
  24. result = httpclient.execute(post);
  25. if (HttpStatus.SC_OK == result.getStatusLine().getStatusCode()) {
  26. // 返回结果
  27. resData = EntityUtils.toString(result.getEntity());
  28. }
  29. } finally {
  30. if (result != null) {
  31. result.close();
  32. }
  33. if (post != null) {
  34. post.releaseConnection();
  35. }
  36. httpclient.close();
  37. }
  38. return resData;
  39. }
  40. // 生成header的方法
  41. private static Map<String, String> generateHeader(String callbackUrlParam) {
  42. Map<String, String> headers = new HashMap<>();
  43. // 商户结算平台 id,固定
  44. headers.put('appkey', Constant.appkey);
  45. // 请求标识 ,每次请求唯一,动态数据
  46. String requestID = UUID.randomUUID().toString();
  47. headers.put('request_id', requestID);
  48. // 时间戳(秒),30 分钟过期,动态数据
  49. Date date = new Date();
  50. String timestamp = String.valueOf(date.getTime() / 1000);
  51. headers.put('timestamp', timestamp);
  52. // 签名方法 ,暂支持 sha256
  53. headers.put('sign_type', 'sha256');
  54. // 签名,算法为 HMACSHA256(appkey+timestamp+appsecret),动态生成
  55. String signatureStr = Constant.appkey + timestamp + Constant.appsecret;
  56. String signature = null;
  57. try {
  58. signature = EncryptUtil.HMACSHA256(signatureStr, Constant.appsecret);
  59. } catch (Exception e) {
  60. e.printStackTrace();
  61. }
  62. headers.put('signature', signature);
  63. // 版本, 本文档为 2.0
  64. headers.put('version', '2.0');
  65. // 回调地址, 以 https://或者 http://开头并进行 base64 编码
  66. String callbackUrl = callbackUrlParam;
  67. if (callbackUrl != null) {
  68. // 需要对callbackUrlParam进行base64 编码,然后赋值给 callbackUrl
  69. Base64.Encoder encoder = Base64.getEncoder();
  70. byte[] textByte;
  71. try {
  72. textByte = callbackUrlParam.getBytes('UTF-8');
  73. callbackUrl = encoder.encodeToString(textByte);
  74. headers.put('callback_url', callbackUrl);
  75. } catch (UnsupportedEncodingException e) {
  76. e.printStackTrace();
  77. }
  78. }
  79. // 项目编号,暂不支持
  80. headers.put('project_code', null);
  81. return headers;
  82. }

七 根据API文档,编写必要的DTO

针对每个API,主要包含请求DTO,响应DTO,回调响应DTO,这个就要跟踪API要求,编写满足要求的DTO。

当然有些DTO是可以抽象成一个类,例如一般响应DTO和回调响应DTO都是一样的,这个时候就可以抽象为一个DTO了。

例如:

  1. package GDDto;
  2. /**
  3. * @className: GDCommonRes
  4. * @description: 共同响应结果
  5. * @date: 2020/9/24
  6. * @author: cakin
  7. */
  8. public class GDCommonRes {
  9. /**
  10. * 状态码
  11. */
  12. private int code;
  13. /**
  14. * 消息
  15. */
  16. private String msg;
  17. /**
  18. * 请求时的 request_id
  19. */
  20. private String request_id;
  21. /**
  22. * 数据
  23. */
  24. private String data;
  25. /**
  26. * appkey
  27. */
  28. private String appkey;
  29. public int getCode() {
  30. return code;
  31. }
  32. public void setCode(int code) {
  33. this.code = code;
  34. }
  35. public String getMsg() {
  36. return msg;
  37. }
  38. public void setMsg(String msg) {
  39. this.msg = msg;
  40. }
  41. public String getRequest_id() {
  42. return request_id;
  43. }
  44. public void setRequest_id(String request_id) {
  45. this.request_id = request_id;
  46. }
  47. public String getData() {
  48. return data;
  49. }
  50. public void setData(String data) {
  51. this.data = data;
  52. }
  53. public String getAppkey() {
  54. return appkey;
  55. }
  56. public void setAppkey(String appkey) {
  57. this.appkey = appkey;
  58. }
  59. }

八 针对每个API方法,进行对接

做好以上准备工作,就可以一个个接口进行对接了。可采用下面方式一个一个接口进行对接。

  1. public static void main(String[] args) {
  2. // 1. 批量创建结算单 回调接口。
  3. // batchSettlement();
  4. // 2. 查询结算单 非回调接口。
  5. //querySettlement();
  6. // 3. 结算单退款 回调接口。
  7. refundSettlement();
  8. // 4. 授权签约 非回调接口。
  9. // authorizSigned();
  10. // 5. 获取签约结果 非回调接口。
  11. // querySigned();
  12. }

九 对接代码结构

十 一些对接技巧

1 遇到问题,如果需要公司B的帮助,需要主动和公司B的对接人员交流,尽快找到问题所在。

2 有些基础代码,如果公司B能提供,主动要一下,如果确实因为信息安全问题,公司B不方面提供,那就得自己写了,写完后,如果不确定代码是否符合B的要求,可以发给公司B的对接人员看看,以确定代码的正确性。

3 学习公司B的接口文档中好的地方,应用到自己的工作中。

4 委婉指出接口文档中的错误和不足,帮助公司B文档质量改进,这样在对接时,公司B的对接人员也会更热心的帮助你。

5 公司A的对接代码,放到正式代码的test目录中,一来可以方便调用正式代码中的工具类,二来方便将对接代码移植到正式代码中。

(0)

相关推荐