20 KiB
驿公里自营优惠券三方发放对接标准文档
一、业务时序图
二、接口鉴权
- 简要描述
驿公里开放所有接口均需根据鉴权流程对参数进行指定封装,以下api返回均默认忽略加解密过程,具体加解密参见如下表述:
- 邮件申请驿公里API调用权限,请提供以下信息(发送:gyy@haixiuyizhan.com(龚云云) 抄送:li.chenyang@haixiuyizhan.com(李晨阳),czy@haixiuyizhan.com(蔡哲义),ma.xiaoda@haixiuyizhan.com(马孝达))
邮件申请请求样例
我司现需申请贵公司驿公里开放平台业务开放接口权限。
提供数据如下:
1.公司名称:XXXX
2.紧急联系人姓名:XXX
3.紧急联系人电话:XXXXXXX
4.请求服务测试环境IP(多个用逗号分割):xxx.xxx.xxx.xxx
5.请求服务生产环境IP(多个用逗号分割):xxx.xxx.xxx.xxx
6.需要请求的接口地址(多个用逗号分割):xxx/xxx
请求方会申请到以下信息,在请求接口时请按流程进行封装
| 字段名 | 必选 | 类型 | 说明 |
| devId | 是 | string | 开发者账号 |
| desKey | 是 | string | des秘钥 |
| publicKey | 是 | string | 三方公钥和驿公里公钥,用来验签 |
| privateKey | 是 | String | 三方私钥,用来加签 |
- 接口请求示例
{
"header": {
"devId": "9999999999",
"sign": "ZYWPTWM61gNzyAy62Jcx6ZpxHDOP/S5MZeoAgxt5C1AZ85vfx7bASTQA5mXCjPjM7Mylfvd3hJkvRZ9S+u7RbQBw6u/IPVSumVgwmYveRLOKpY4+LGxih12XFuw1YZHwN+swMBRPuNMsUug0vK+j6V4ivNtx4kr47nvsRU5vX9GJAdaDm+TVEP9NfbpvHQrpKeq1nLRKZhbHghH+ShID8vaYJaUQTC4+ckmyfWaTbVBm6bFDFVoaJpEopSlGXz88SWNF51LyPrqztWr/OgUxrgIczsmbQVi7Kyy4eUCLBUi8DdckUgs7ALKbF/EClyYVTp1evbP7cX0tKs470+Qo3Q==",
"timeStamp": "1726655501583"
},
"body": "rdT9YyNphdvYdZ8HOXBip3SzvC9Xvz1TJ9nK5WdOFqhzUOW1n7u2giaDZifBpKqWkR0/6g5zs+YfQykWi4pUytzgOxysrkwY+o8RtKI8lD9RLy7kXTtdVMY1/v91suqhRNQ+2MmDgAGTI9G2NnhhLXQLK8YeNQUcl8T64kgZ6gNcEAxrKiDcNiBjfIPLV+UwfKXd4/LE+KSycy7MSB+DYy3oZwrN5FHsAwgUamIFyrc="
}
- 请求参数说明
| 参数名 | 类型 | 说明 |
| body | string | 加密后的请求api请求参数 |
| header.devId | string | 请求方身份 |
| sign | string | 签名信息 |
| timeStamp | string | 时间戳 |
- 返回示例
{
"traceId": "1660552768027423965455945",
"success": true,
"resultCode": "SUCCESS",
"errorMsg": null,
"extInfo": null,
"resultInfo": {
"header": {
"devId": "9999999999",
"sign": "fSpmk+DBtiJ2nL3tNKt460pWdbuZc9zFkSvJdAl7Gx023up8wA4qN6Ez+GQ5Wp7SEteOCHvteSaEwgBv/euLo8kE8nmwB0jGqB/I7WMESDUTjWBUOiXJrA2/OPO8m0v947EuTSOqCM4ILWX9NY+qknqB73dRTjJ1O1YjoElTsBNHlmwR0GNV3Q5aE/GS1ZZYfI38e5ACVZvDgwVgrWEaQ0dPgVpcnInLXzeryeY6+iVcK/QtyGrL/7Fi51IHQ0p7wGOpSFY6dXZYWiidz2r8tme6HJgVehFqmQb8SMMSCEswZYBa1mfmxMlwxwo6i6MUl3pZESs9u9xcpdTpEun67Q==",
"timeStamp": "1660552772062"
},
"body": "D9MxbcjyysV5WCOaV27gEH2S/TXYLoWMp6+dcfx5jy9O+uI77AN5xmrQUEpQkVvdoKtc8sE7KaO26NaUobRXrw=="
}
}
- 返回参数说明
| 参数名 | 类型 | 说明 |
| traceId | string | 请求唯一码 |
| success | boolean | 请求成功or失败 |
| resultCode | String | 请求响应码 |
| errorMsg | string | 错误提示信息 |
| extInfo | object | 额外信息 |
| resultInfo | object | 返回数据 |
| body | string | 加密后的数据信息 ,注意:后续接口返回结果均为本字段解密后的结果,不做另外说明 |
| header.devId | string | 请求方身份 |
| header.sign | string | 签名信息 |
| header.timeStamp | string | 时间戳 |
- 加密流程
- 加密方法(请求驿公里前请将参数加密):transmissionData
- 解密方法(获取到的返回结果请解密后使用):parseRequest
public class SecurityUtil {
public static final String CHARSET = "UTF-8";
public SecurityUtil() {
}
private static final byte[] decryptBASE64(String key) {
return Base64.decodeBase64(key);
}
private static final String encryptBASE64(byte[] key) {
return Base64.encodeBase64String(key);
}
/**
* 通过这个方法解密响应数据中的body字段,得到真正的业务数据
*
* @param jsonObject body原文
* @param cryptData 响应数据中的body字段值
* @param desKey 申请到的desKey
* @param charset "utf-8"
* @return 业务响应数据
*/
public static final String decryptDes(String cryptData, String key, String charset) {
try {
return new String(DESCoder.decrypt(decryptBASE64(cryptData), decryptBASE64(key)), charset);
} catch (Exception var4) {
throw new RuntimeException("解密错误,错误信息:", var4);
}
}
public static final String encryptDes(String data, String key, String charset) {
try {
return encryptBASE64(DESCoder.encrypt(data.getBytes(charset), decryptBASE64(key)));
} catch (Exception var4) {
throw new RuntimeException("加密错误,错误信息:", var4);
}
}
public static final String signRSA(String data, String privateKey, String charset) {
try {
return encryptBASE64(sign(data.getBytes(charset), decryptBASE64(privateKey)));
} catch (Exception var4) {
throw new RuntimeException("签名错误,错误信息:", var4);
}
}
public static final boolean verifyRSA(String data, String publicKey, String sign, String charset) {
try {
return verify(data.getBytes(charset), decryptBASE64(publicKey), decryptBASE64(sign));
} catch (Exception var5) {
throw new RuntimeException("验签错误,错误信息:", var5);
}
}
public static byte[] sign(byte[] data, byte[] privateKey) throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(priKey);
signature.update(data);
return signature.sign();
}
public static boolean verify(byte[] data, byte[] publicKey, byte[] sign) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(sign);
}
/**
* 加密数据
*
* @param jsonObject body原文
* @param devId 申请到的开发者ID
* @param desKey 申请到的desKey
* @param privateKey privateKey
* @return
*/
public static final DataBean transmissionData(JSONObject jsonObject, String desKey, String privateKey, String devId) {
//body密文
String content = SecurityUtil.encryptDes(jsonObject.toJSONString(), desKey, CHARSET);
//加签
String signBack = SecurityUtil.signRSA(jsonObject.toJSONString(), privateKey, CHARSET);
DataBean dataBean = new DataBean();
dataBean.setBody(content);
DataBean.Header header = new DataBean.Header();
header.setDevId(devId);
header.setSign(signBack);
header.setTimeStamp(String.valueOf(System.currentTimeMillis()));
dataBean.setHeader(header);
return dataBean;
}
/**
* 解密数据
*/
private static void parseRequest(String bodyStr,String desKey,String publicKey) {
// 转JSONObject
JSONObject requestBody = JSONObject.parseObject(bodyStr);
// 1. 获取消息体中key为"body"的value值
String body = requestBody.getString("body");
if (StringUtils.isEmpty(body)) {
throw new IllegalArgumentException("消息体中key为body的value值为空");
}
// 2.获取消息体中key为"header"的value值
String header = requestBody.getString("header");
if (StringUtils.isEmpty(header)) {
throw new IllegalArgumentException("消息体中key为header的value值为空");
}
DataBean.Header headerBody = JSONObject.parseObject(header, DataBean.Header.class);
// 根据解析出来的headerBody对象获取指定的属性值
String devId = headerBody.getDevId();
if (StringUtils.isEmpty(devId)) {
throw new IllegalArgumentException("devId为空");
}
String sign = headerBody.getSign();
if (StringUtils.isEmpty(sign)) {
throw new IllegalArgumentException("sign为空");
}
// 解密消息体中的body值
String data = SecurityUtil.decryptDes(body, desKey, SecurityUtil.CHARSET);
// 验签
if (!SecurityUtil.verifyRSA(data, publicKey, sign, SecurityUtil.CHARSET)) {
throw new IllegalArgumentException("验签不通过");
}
HashMap<String, String> map = new HashMap(4);
map.put("devId", devId);
map.put("data", data);
System.out.println("解密后的数据为: "+JSONObject.toJSONString(map));
}
public static void main(String[] args) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name","sdfsfsf");
//加密数据
DataBean dataBean = transmissionData(jsonObject, "${desKey}", "${privateKey}", "${devId}");
System.out.println("加密后的数据为: "+JSONObject.toJSONString(dataBean));
//解密数据
parseRequest(JSONObject.toJSONString(dataBean),"${desKey}","${publicKey}");
}
}
@Data
class DataBean {
private Header header;
private String body;
@Data
static class Header {
private String devId;
private String sign;
private String timeStamp;
}
}
class DESCoder {
public DESCoder() {
}
private static SecretKey toKey(byte[] key) {
SecretKey k = new SecretKeySpec(key, "DES");
return k;
}
public static byte[] decrypt(byte[] data, byte[] key) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException {
SecretKey k = toKey(key);
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5PADDING", "BC");
cipher.init(2, k);
return cipher.doFinal(data);
}
public static byte[] encrypt(byte[] data, byte[] key) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException, NoSuchProviderException {
Key k = toKey(key);
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5PADDING", "BC");
cipher.init(1, k);
return cipher.doFinal(data);
}
}
三、API调用说明
3.1 baseUrl的值根据环境区分
3.2 返回参数说明
{
"traceId": "1722324871690553293106291",
"success": true,
"resultCode": "SUCCESS",
"errorMsg": null,
"extInfo": {
"requestTime": "1722324880384",
"message": "操作成功"
},
"resultInfo": {
"header": {
"devId": "9999999999",
"sign": "AOkgoadCmRRvT+IoA0A1DpMf7rR0D84lmbEeAh4aN/XWJ1iIfn35a7wrsFWFoevIeD0RpKLhwxVuxnnvUrjFECWvYs5W6M71UT8AXnZBEfXTqnZRwtywjKVmoctfYRinEphj/attADj2vXZsyGVZYzIiak782Cz0TC5fJsB5eLT4owwd6mVyOnmp4pfsfiXwCCA6XqyOZzP6AFc1hPU09fnLbu9Dwrdw1LUg8+M2C8dwO4zK9FQxd2WW5e7miRMqtQIj3eT9wD7Xca/3yD+NcZ+Yb4JYQLnqjw4GWw9iVYnCrSS/CnsqoRi0jeM9vcYuQR+a5kKn06Dh5SuTwThA/w==",
"timeStamp": "1722324925168"
},
"body": "uGHNuwp5DTmcr/iNDgTK1L+2byrJiQ+Uy8uXCgmPqn2pe3FJrvgnnvorBiAdDWDuiY9e+rK1+Qbpf1eDLSdBsxgOFv+ScrkjNfD4BHsHOClMB6E/8qr2VekH82puteUdmdXa0I+w8f0="
}
}
| 参数名 | 类型 | 说明 |
| traceId | String | 请求唯一码 |
| success | boolean | 请求成功or失败 |
| resultCode | String | 请求响应码 |
| erroMsg | String | 错误提示信息 |
| extInfo | Map | 额外信息 |
| resultInfo | object | 返回数据 |
| resultInfo.header.devId | String | 请求方身份 |
| resultInfo.header.sign | String | 签名信息 |
| resultInfo.body | String | 加密后的数据信息 |
- success等于true 或者 resultCode等于"SUCCESS"表示请求成功
- 请求成功了,如果有数据返回则在resultInfo里面
- 注意:此文档以下的API返回结果中的resultInfo均为resultInfo.body解密后的结果,不做另外说明
四、开放平台接口设计
4.1 优惠券基本信息查询接口
- 请求路径:{{baseUrl}}/open/yglOpenPlatform/getTicketPackageInfo
- 请求方法:POST application/json
- 请求入参:
{
"ticketPackageCode": "32a982ad16984ab1bd3f7aa7d860e0c6",
"mobile":"17800000000",
"extParams":{}
}
| 参数 | 示例 | 是否必填 | 类型 | 备注 |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | 是 | String | 用户需要购买的券包码 |
| mobile | 17800000000 | 是 | String | 用户手机号 |
| extParams | 否 | JSONObject | 拓展字段,非必传 |
- 返回参数
{
"traceId": null,
"success": true,
"resultCode": "SUCCESS",
"errorMsg": null,
"extInfo": null,
"resultInfo": {
"devId": "9999999999",
"data": {
"couponType": 1,
"endTime": "2024-09-30 23:59:59",
"startTime": "2024-09-18 00:00:00",
"ticketNum": 1,
"ticketPackageName": "三方通用单券券包测试"
}
}
}
| 参数 | 示例 | 类型 | 备注 |
| couponType | 1 | Integer | 券包类型(1 单券包单券/2 单券包多券) |
| startTime | 2024-09-18 00:00:00 | String | 购买有效期开始时间 |
| endTime | 2024-09-30 23:59:59 | String | 购买有效期结束时间 |
| ticketNum | 1 | Integer | 优惠券数量(一个券包中有几张优惠券就返回几) |
| ticketPackageName | 三方通用单券券包测试 | String | 券包名称 |
建议:由三方系统根据返回信息自行判断优惠券是否可以购买,避免发生用户购买后优惠券无法发放的问题
4.2 优惠券发放接口
- 请求路径:{{baseUrl}}/open/yglOpenPlatform/generateTicket
- 请求方法:POST application/json
- 请求入参:
{
"ticketPackageCode": "32a982ad16984ab1bd3f7aa7d860e0c6",
"mobile": "17800000000",
"orderId": "123321123324",
"couponType": 1,
"amount": {
"totalPrice": "299",
"settlementPrice": "299"
}
}
| 参数 | 示例 | 是否必填 | 类型 | 备注 |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | 是 | String | 用户需要购买的券包码(向驿公里申请获得) |
| mobile | 17800000000 | 是 | String | 用户手机号 |
| orderId | 123321123324 | 是 | String | 三方系统订单号 |
| couponType | 1 | 否 | Integer | 券包类型(1 单券包单券/2 单券包多券) |
| amount.totalPrice | 299 | 是 | BigDecimal | 订单原价,单位分 |
| amount.settlementPrice | 299 | 是 | BigDecimal | 订单结算价,单位分 |
- 返回参数
{
"traceId": null,
"success": true,
"resultCode": "SUCCESS",
"errorMsg": null,
"extInfo": null,
"resultInfo": {
"devId": "9999999999",
"data": {
"orderId": "123321123324",
"ticketList": [
{
"isUsed": 0,
"status": 0,
"ticketEndTime": "2024-12-18 23:59:59",
"ticketId": "1836651122727530498",
"ticketName": "三方通用发券单券测试",
"ticketPackageCode": "32a982ad16984ab1bd3f7aa7d860e0c6",
"ticketStartTime": "2024-09-19 14:18:46"
}
],
"ticketPackageCode": "32a982ad16984ab1bd3f7aa7d860e0c6"
}
}
}
| 参数 | 示例 | 类型 | 备注 |
| orderId | 123321123324 | String | 三方系统订单id |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | String | 用户需要购买的券包码(向驿公里申请获得) |
| ticketList.isUsed | 0 | Integer | 0未使用、1已使用 |
| ticketList.status | 0 | Integer | 0有效、1无效 |
| ticketList.ticketStartTime | 2024-09-19 14:18:46 | String | 优惠券有效开始时间 |
| ticketList.ticketEndTime | 2024-12-18 23:59:59 | String | 优惠券有效结束时间 |
| ticketList.ticketId | 1836651122727530498 | String | 优惠券id |
| ticketList.ticketName | 三方通用发券单券测试 | String | 优惠券名称 |
4.3 优惠券取消接口
- 请求路径:{{baseUrl}}/open/yglOpenPlatform/cancelTicket
- 请求方法:POST application/json
- 请求入参:
{
"ticketPackageCode": "32a982ad16984ab1bd3f7aa7d860e0c6",
"orderId": "123321123324",
"refundReason": "退款测试",
"ticketIds": [
"1836368497303564289"
]
}
| 参数 | 示例 | 是否必填 | 类型 | 备注 |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | 是 | String | 用户需要购买的券包码 |
| orderId | 123321123324 | 是 | String | 三方系统订单id |
| refundReason | 退款测试 | 是 | String | 退款理由 |
| ticketIds | ["1836368497303564289"] | 是 | ArrayList | 退券的优惠券id列表 |
- 返回参数
{
"traceId": null,
"success": true,
"resultCode": "SUCCESS",
"errorMsg": null,
"extInfo": null,
"resultInfo": {
"devId": "9999999999",
"data": "优惠券回收成功"
}
}
| 参数 | 示例 | 类型 | 备注 |
| data | 优惠券回收成功 | String | 回收结果 |
五、合作方需提供的接口标准
合作方如果接口返回异常,驿公里侧会在一小时内进行3次重试,3次结果皆失败后不再重试回调
5.1 优惠券核销回调接口
- 请求路径:由合作方提供
- 请求方法:POST application/json
- 请求入参:
{
"orderId": "1836608722336997377",
"ticketPackageCode":"32a982ad16984ab1bd3f7aa7d860e0c6",
"ticketId":"1836344565135241217",
"useTime":"2024-09-18 18:00:37",
"factory":{
"factoryId":"4345",
"factoryName":"滨江区区政府38",
"factoryAddress":"xxxxxx"
},
"orderPrice":{
"price":"1500",
"discountPrice":"1500",
"orderPrice":"0"
}
}
| 参数 | 示例 | 是否必填 | 类型 | 备注 |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | 是 | String | 用户所使用的优惠券券包码 |
| orderId | 1836608722336997377 | 是 | String | 驿公里的订单id |
| ticketId | 1836344565135241217 | 是 | String | 用户所使用的优惠券id |
| useTime | 2024-09-18 18:00:37 | 是 | String | 核销时间 |
| factory.factoryId | 4345 | 是 | String | 核销门店id |
| factory.factoryName | 滨江区区政府38 | 是 | String | 核销门店名称 |
| factory.factoryAddress | xxxxxx | 是 | String | 核销门店地址 |
| orderPrice.price | 1500 | 是 | BigDecimal | 订单原价,单位分 |
| orderPrice.discountPrice | 1500 | 是 | BigDecimal | 订单折扣价,单位分 |
| orderPrice.payPrice | 0 | 是 | BigDecimal | 订单实付金额,单位分 |
- 返回参数
{
"success": true,
"errorMsg": null
}
| 参数 | 示例 | 类型 | 备注 |
| success | true | boolean | 接口请求结果 |
| errorMsg | 错误信息 | String | 如果接口请求失败,请写明失败原因 |
5.2 优惠券退回回调接口
- 请求路径:由合作方提供
- 请求方法:POST application/json
- 请求入参:
{
"orderId": "1836608722336997377",
"ticketPackageCode":"32a982ad16984ab1bd3f7aa7d860e0c6",
"ticketId":"1836344565135241217",
"refundTime":"2024-09-18 18:00:37"
}
| 参数 | 示例 | 是否必填 | 类型 | 备注 |
| ticketPackageCode | 32a982ad16984ab1bd3f7aa7d860e0c6 | 是 | String | 用户所使用的优惠券券包码 |
| orderId | 1836608722336997377 | 是 | String | 驿公里的订单id |
| ticketId | 1836344565135241217 | 是 | String | 用户所使用的优惠券id |
| refundTime | 2024-09-18 18:00:37 | 是 | String | 核销时间 |
- 返回参数
{
"success": true,
"errorMsg": null
}
| 参数 | 示例 | 类型 | 备注 |
| success | true | boolean | 接口请求结果 |
| errorMsg | 错误信息 | String | 如果接口请求失败,请写明失败原因 |