哆瑞咪购卡接口文档
采购代理商购卡接口文档
本文档使用 MrDoc 发布
-
+
首页
采购代理商购卡接口文档
# 开放平台接口文档 ## 概述 本文档描述了开放平台提供的预付费卡相关API接口,供第三方机构对接使用。所有接口均采用HTTPS协议,支持多种加密算法和签名验证机制。 ## 接口基础信息 - **接口协议**: HTTPS - **请求方式**: POST - **数据格式**: JSON - **字符编码**: UTF-8 - **时间格式**: Unix时间戳(秒) - **签名算法**: 支持MD5、SM3、RSA2、SM2 - **加密算法**: 支持AES、SM4 ## 安全机制 ### 1. 参数签名 所有请求参数需要进行签名验证,签名步骤如下: 1. 将请求参数按照参数名的字典序排列 2. 按照`key=value&key=value`的格式拼接参数字符串 3. 根据配置的签名算法生成签名 4. 将签名添加到请求参数中的`sign`字段 **签名算法示例(MD5)**: ```java // 1. 参数排序并拼接 String signData = "buyCount=1&faceValue=100&outTradeNo=TEST123&userPhone=13800138000"; // 2. 添加密钥并计算MD5 String sign = MD5(signData + "&key=" + signKey); ``` ### 2. 数据加密 敏感数据(如卡密信息)采用对称加密算法进行加密传输: **AES加密示例**: ```java // 使用AES-ECB-PKCS5Padding模式 AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, HexUtil.decodeHex(secretKey)); String encrypted = aes.encryptBase64(plaintext, StandardCharsets.UTF_8); ``` **AES解密示例**: ```java AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, HexUtil.decodeHex(secretKey)); String decrypted = aes.decryptStr(Base64.decode(ciphertext), StandardCharsets.UTF_8); ``` ## 接口列表 ### 1. 企业购卡接口 #### 接口信息 - **接口地址**: `/card/create/{partnerId}` - **请求方式**: POST - **接口描述**: 企业批量购买预付费卡 #### 请求参数 | 参数名 | 类型 | 必填 | 描述 | 示例值 | |--------|------|------|------|--------| | buyCount | Integer | 是 | 购卡数量 | 1 | | faceValue | BigDecimal | 是 | 购卡面值(元) | 100.00 | | userPhone | String | 否 | 用户手机号(AES加密) | "base64加密字符串" | | outTradeNo | String | 是 | 商户订单号 | TEST123456789 | | sign | String | 是 | 签名 | 32位MD5签名 | #### 请求示例 ```json { "buyCount": 1, "faceValue": 100.00, "userPhone": "U2FsdGVkX1+vupppZksvRf5pq5g5XjFRIipRkwB0K1Y=", "outTradeNo": "TEST123456789", "sign": "5d41402abc4b2a76b9719d911017c592" } ``` #### 请求Demo(Java) ```java public void testBuyCard() { String partnerId = "A1745547325"; String signKey = "gjU6yYgWJD0KqCx8"; String secret = "b078e3f34dba78e26f0e3dfcd380c0ea"; String outTradeNo = "TEST" + System.currentTimeMillis(); // 1. 构建请求参数 Map<String, Object> params = new TreeMap<>(); params.put("buyCount", 1); params.put("faceValue", new BigDecimal("100.00")); params.put("outTradeNo", outTradeNo); // 2. 如需绑定用户,加密手机号 String userPhone = "13800138000"; String encryptedPhone = encryptAes(secret, userPhone); params.put("userPhone", encryptedPhone); // 3. 生成签名 String signData = getSignData(params); String sign = SecureUtil.md5(signData + "&key=" + signKey); params.put("sign", sign); // 4. 发送请求 String url = "https://pre-pay-test.duormi.com/api/card/create/" + partnerId; HttpResponse response = HttpUtil.createPost(url) .contentType(ContentType.JSON.toString()) .body(JSON.toJSONString(params)) .execute(); // 5. 处理响应 String responseBody = response.body(); JSONObject responseJson = JSON.parseObject(responseBody); if (responseJson.getInteger("code") == 0) { JSONObject data = responseJson.getJSONObject("data"); String cardPwdList = data.getString("cardPwdList"); // 6. 如果返回了卡密列表,进行解密 if (cardPwdList != null && !cardPwdList.isEmpty()) { String decryptedCardList = decryptAes(secret, cardPwdList); JSONArray cardList = JSON.parseArray(decryptedCardList); for (int i = 0; i < cardList.size(); i++) { JSONObject card = cardList.getJSONObject(i); System.out.println("卡密: " + card.getString("cardPwd")); System.out.println("余额: " + card.getString("balance")); System.out.println("面值: " + card.getString("faceValue")); System.out.println("有效期: " + card.getString("validateTime")); } } } } // 签名数据生成方法 private String getSignData(Map<String, Object> params) { return params.entrySet().stream() .filter(entry -> !"sign".equals(entry.getKey()) && entry.getValue() != null) .sorted(Map.Entry.comparingByKey()) .map(entry -> entry.getKey() + "=" + entry.getValue()) .collect(Collectors.joining("&")); } // AES加密方法 public static String encryptAes(String key, String plaintext) { AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, HexUtil.decodeHex(key)); return aes.encryptBase64(plaintext, StandardCharsets.UTF_8); } // AES解密方法 public static String decryptAes(String key, String cipherBase64) { AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, HexUtil.decodeHex(key)); return aes.decryptStr(Base64.decode(cipherBase64), StandardCharsets.UTF_8); } ``` #### 响应参数 | 参数名 | 类型 | 描述 | 示例值 | |--------|------|------|--------| | code | Integer | 响应码,0表示成功 | 0 | | msg | String | 响应消息 | "操作成功" | | data | Object | 响应数据 | - | | ├─ partnerId | String | 企业编号 | "A1745547325" | | ├─ outOrderNo | String | 外部订单号 | "TEST123456789" | | ├─ cardMedia | Integer | 媒介类型:1-实体卡,2-电子卡 | 2 | | ├─ faceValue | BigDecimal | 购卡面值(元) | 100.00 | | ├─ buyCount | Integer | 购买数量 | 1 | | ├─ amount | BigDecimal | 支付总金额(元) | 100.00 | | ├─ cardPwdList | String | 卡密列表(加密) | "base64加密字符串" | | ├─ availableBalance | BigDecimal | 可用余额(元) | 9900.00 | | ├─ buyStatus | Integer | 购卡状态:1-成功,-1-失败,0-购卡中 | 1 | | ├─ applyTime | Long | 申请时间(时间戳) | 1704085200 | | ├─ orderNo | String | 购卡订单号 | "SYS202401011200001" | | sign | String | 响应签名 | "32位MD5签名" | #### 卡密列表解密 响应中的`cardPwdList`字段为加密数据,需要使用配置的密钥进行解密: ```java // 解密卡密列表 String decryptedCardList = decryptAes(secretKey, cardPwdList); // 解析JSON数组 JSONArray cardList = JSON.parseArray(decryptedCardList); for (int i = 0; i < cardList.size(); i++) { JSONObject card = cardList.getJSONObject(i); String cardPwd = card.getString("cardPwd"); // 卡密 String balance = card.getString("balance"); // 余额 String faceValue = card.getString("faceValue"); // 面值 String validateTime = card.getString("validateTime"); // 有效期 } ``` #### 手机号加密说明 如果请求中包含`userPhone`字段,需要使用AES加密: ```java // 加密手机号 String encryptedPhone = encryptAes(secretKey, "13800138000"); ``` **业务逻辑**: - 如果上送了`userPhone`,系统会直接为该手机号对应的用户购卡并绑定,响应中不返回`cardPwdList` - 如果未上送`userPhone`,系统会返回加密的`cardPwdList`供后续分发使用 #### 响应示例 **场景1:未上送手机号(返回卡密列表)** ```json { "code": 0, "msg": "操作成功", "data": { "partnerId": "A1745547325", "outOrderNo": "TEST123456789", "cardMedia": 2, "faceValue": 100.00, "buyCount": 1, "amount": 100.00, "cardPwdList": "U2FsdGVkX1+vupppZksvRf5pq5g5XjFRIipRkwB0K1Y=", "availableBalance": 9900.00, "buyStatus": 1, "applyTime": 1704085200, "orderNo": "SYS202401011200001" }, "sign": "5d41402abc4b2a76b9719d911017c592" } ``` **场景2:上送手机号(直接绑定用户)** ```json { "code": 0, "msg": "操作成功", "data": { "partnerId": "A1745547325", "outOrderNo": "TEST123456789", "cardMedia": 2, "faceValue": 100.00, "buyCount": 1, "amount": 100.00, "availableBalance": 9900.00, "buyStatus": 1, "applyTime": 1704085200, "orderNo": "SYS202401011200001" }, "sign": "5d41402abc4b2a76b9719d911017c592" } ``` ### 2. 购卡查询接口 #### 接口信息 - **接口地址**: `/card/query/{partnerId}` - **请求方式**: POST - **接口描述**: 查询企业购卡订单状态和详情 #### 请求参数 | 参数名 | 类型 | 必填 | 描述 | 示例值 | |--------|------|------|------|--------| | outTradeNo | String | 是 | 商户订单号 | TEST123456789 | | sign | String | 是 | 签名 | 32位MD5签名 | #### 请求示例 ```json { "outTradeNo": "TEST123456789", "sign": "5d41402abc4b2a76b9719d911017c592" } ``` #### 请求Demo(Java) ```java public void testQueryCard() { String partnerId = "A1745547325"; String signKey = "gjU6yYgWJD0KqCx8"; String outTradeNo = "TEST123456789"; // 1. 构建请求参数 Map<String, Object> params = new TreeMap<>(); params.put("outTradeNo", outTradeNo); // 2. 生成签名 String signData = getSignData(params); String sign = SecureUtil.md5(signData + "&key=" + signKey); params.put("sign", sign); // 3. 发送请求 String url = "https://pre-pay-test.duormi.com/api/card/query/" + partnerId; HttpResponse response = HttpUtil.createPost(url) .contentType(ContentType.JSON.toString()) .body(JSON.toJSONString(params)) .execute(); // 4. 处理响应 String responseBody = response.body(); System.out.println("响应结果: " + responseBody); } ``` #### 响应参数 响应参数与购卡接口相同,详见上述购卡接口响应参数说明。 #### 响应示例 查询接口的响应格式与购卡接口相同,根据原始购卡请求是否包含手机号返回不同内容: **场景1:原购卡请求未上送手机号(返回卡密列表)** ```json { "code": 0, "msg": "操作成功", "data": { "partnerId": "A1745547325", "outOrderNo": "TEST123456789", "cardMedia": 2, "faceValue": 100.00, "buyCount": 1, "amount": 100.00, "cardPwdList": "U2FsdGVkX1+vupppZksvRf5pq5g5XjFRIipRkwB0K1Y=", "availableBalance": 9900.00, "buyStatus": 1, "applyTime": 1704085200, "orderNo": "SYS202401011200001" }, "sign": "5d41402abc4b2a76b9719d911017c592" } ``` **场景2:原购卡请求上送了手机号(已绑定用户)** ```json { "code": 0, "msg": "操作成功", "data": { "partnerId": "A1745547325", "outOrderNo": "TEST123456789", "cardMedia": 2, "faceValue": 100.00, "buyCount": 1, "amount": 100.00, "availableBalance": 9900.00, "buyStatus": 1, "applyTime": 1704085200, "orderNo": "SYS202401011200001" }, "sign": "5d41402abc4b2a76b9719d911017c592" } ``` ## 错误码说明 | 错误码 | 错误信息 | 描述 | |--------|----------|------| | 0 | 操作成功 | 请求处理成功 | | 400 | 参数错误 | 请求参数格式错误或缺少必填参数 | | 401 | 签名验证失败 | 请求签名验证不通过 | | 403 | 权限不足 | 企业不存在或已停用 | | 500 | 系统错误 | 服务器内部错误 | | 1001 | 余额不足 | 代理商余额不足,无法完成购卡 | | 1002 | 订单已存在 | 商户订单号重复 | | 1003 | 订单不存在 | 查询的订单不存在 | ## 接入流程 ### 1. 申请接入 1. 联系平台申请开通开放平台权限 2. 获取企业编号(partnerId) 3. 配置签名密钥和加密密钥 4. 获取接口调用地址 ### 2. 开发对接 1. 根据本文档实现签名和加密逻辑 2. 在测试环境进行接口调试 3. 完成功能测试和压力测试 4. 申请生产环境上线 ### 3. 生产部署 1. 配置生产环境密钥 2. 更新接口调用地址 3. 监控接口调用状态 4. 建立异常处理机制 ## 技术支持 如有技术问题,请联系: - 技术支持邮箱:fufengyuan@duormi.com - 技术支持微信:fufengyuan111 - 工作时间:周一至周五 9:30-19:00 ## 更新日志 | 版本 | 更新时间 | 更新内容 | |------|----------|----------| | v1.0 | 2025-08-30 | 初始版本,支持企业购卡和查询功能 | --- **注意事项**: 1. 所有金额单位均为人民币元,保留两位小数 2. 时间格式统一使用Unix时间戳(秒) 3. 手机号等敏感信息需要使用AES加密传输 4. 请妥善保管签名密钥和加密密钥,避免泄露 5. 建议在生产环境中使用HTTPS协议确保数据传输安全 6. 接口调用频率限制:每秒最多10次请求
哆瑞咪
2025年8月30日 21:44
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码