易码付平台对接调试
最近公司因公司需要,要求对接易码付聚合支付平台,这里记录一下我的对接流程,有需要代码的可直接复用
1. 页面介绍
右上角开发参数中就是我们所需要的接口参数了
其中Server 端交易密钥是我们每次请求动态生成签名用的,不要告诉别人哟
2. 接口测试前准备
2.1 签名算法编写
签名生成的通用步骤如下:
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。 注意:密钥的长度为32个字节。
◆ API Key 是商户在易码付系统中调用API的重要密钥,请安全存储,确保其不要被泄露。
按照签名生成步骤我们来实现我们的签名算法,并与平台的签名检验工具比较
public static String getSign(Map<String, Object> params, List<String> ignoreKeys, String secretKey) {
Set<String> keySet = params.keySet();
List<String> collect = keySet.stream().filter(key -> ignoreKeys == null || !ignoreKeys.contains(key))
.sorted()
.collect(Collectors.toList());
// 拼接签名字段
StringBuilder builder = new StringBuilder();
for (String signKey : collect) {
builder.append(signKey).append("=").append(params.get(signKey)).append("&");
}
builder.append("key=").append(secretKey);
String signStr = builder.toString();
// 平台生成MD5签名的编码使用的是UTF-8注意我们加密的编码。当结果存在中文字符时不同的编码最终生成的结果是不一样的
String ret = DigestUtils.md5DigestAsHex(signStr.getBytes(StandardCharsets.UTF_8)).toUpperCase();
return ret;
}
易码付签名测试
常见问题
1、注意参数是否区分大小写,参数大小写不正确将会导致签名错误
2、检查所有参数是否与文档完全一致
常见问题:
1、请求数据的编码是否正确,微信支付接口编码要求统一为UTF-8
2、签名原串是否存在被URLencode编码的参数,微信支付的签名原串要求使用参数的原值进行签名
3、请求参数是否存在特殊字符,或者字段长度不符的情况
易码付签名生成
本地代码测试
2.2 公用Post请求接口
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
@Slf4j
public class HttpUtil {
public static String sendPost(String url, Map<String, String> heads, Map<String, Object> body) {
HttpURLConnection conn = null;
OutputStream out = null;
StringBuilder builder = new StringBuilder();
BufferedReader reader = null;
try {
URL urlObj = new URL(url);
//start 这一段代码必须加在open之前,即支持ip访问的关键代码
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
});
conn = (HttpURLConnection) urlObj.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setInstanceFollowRedirects(true);
conn.setRequestMethod("POST");
// 设置共用请求配置信息
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
// 如果请求头参数不为空,则携带请求头
if (heads != null && heads.size() > 0) {
Set<Map.Entry<String, String>> entries = heads.entrySet();
for (Map.Entry<String, String> entry : entries) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
}
// 设置超时时间
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
// 设置 body 参数
out = conn.getOutputStream();
String s = JSON.toJSONString(body);
out.write(s.getBytes(StandardCharsets.UTF_8));
out.flush();
// 发送请求
int responseCode = conn.getResponseCode();
if (HttpStatus.OK.value() != responseCode) {
log.error("{} Http 请求失败,状态码值:{}", url, responseCode);
return null;
}
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
String line = null;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} catch (Exception e) {
log.error("{} 请求异常:", url, e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("HttpUtil 流关闭异常:", e);
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
log.error("HttpUtil 流关闭异常:", e);
}
}
}
return builder.toString();
}
}
3. 接口测试
3.1 创建订单接口
1. 渠道配置
在主页,点击自己的应用,可以查看对应的交易信息,包括支付渠道配置等。创建订单的对应渠道需要在渠道配置中配置,否则订单创建失败。
2. 创建订单代码
public OrderMode<CreateOrderModel> createOrder(CreateOrderDto orderDto) {
// 参数校验:将支付渠道定义为枚举值
if (!PayTypeEnum.checkPayType(orderDto.getPayType())) {
throw new OrderException("支付渠道非法,请检查支付类型参数");
}
// 获取参数
Map<String, Object> params = getCreateOrderParams(orderDto);
log.info("开始请求:{}", params);
// 发起请求
String ret = HttpUtil.sendPost(baseUrl + "/order.html", null, params);
if (StringUtils.isEmpty(ret)) {
throw new OrderException("网络忙,请稍候再试");
}
OrderMode<CreateOrderModel> mode = JSONObject.parseObject(ret, new TypeReference<OrderMode<CreateOrderModel>>() {});
return mode;
}
private Map<String, Object> getCreateOrderParams(CreateOrderDto orderDto) {
Map<String, Object> map = new HashMap<>();
// 应用ID 易码付平台的Appid
map.put("app_id", applId);
// 商户订单号,使用UUID测试
map.put("out_trade_no", UUID.randomUUID().toString().replaceAll("-", "").toUpperCase());
// 支付渠道
map.put("pay_type", orderDto.getPayType());
// 商品描述
map.put("description", orderDto.getDescription());
// 接口传递元,使用字符串接收 BigDecimal 转换为分
map.put("amount", new BigDecimal(orderDto.getAmount()).multiply(new BigDecimal(100)).intValue());
map.put("client_ip", "127.0.0.1");
// 通知URL
map.put("notify_url", "https://822873ew63.goho.co/yimapay/notify");
map.put("time_expire", timeExpire);
// 附加参数:建议在此处转换为 JSON 串,方便后续生成签名
map.put("attach", "{\"userId\":888,\"level\":3}");
// 生成签名(sign 不参与签名生成)
String sign = SignUtil.getSign(map, null, secretKey);
map.put("sign", sign);
return map;
}
3. 请求测试
3.2 查询订单
1. 查询订单代码
public OrderMode<QueryOrderModel> queryOrder(String queryOrder) {
JSONObject param = JSON.parseObject(queryOrder);
String outTradeNo = param.getString("out_trade_no");
String tradeNo = param.getString("trade_no");
Map<String, Object> params = new HashMap<>();
params.put("app_id", applId);
params.put("out_trade_no", outTradeNo);
params.put("trade_no", tradeNo);
// 生成签名
String sign = SignUtil.getSign(params, null, secretKey);
params.put("sign", sign);
// 发起请求
String ret = HttpUtil.sendPost(baseUrl + "/orderid.html", null, params);
if (StringUtils.isEmpty(ret)) {
throw new OrderException("网络忙,请稍候再试");
}
// 接口响应中文使用,URLEncoder加密,解密后再转换为实体类
try {
ret = URLDecoder.decode(ret, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
OrderMode<QueryOrderModel> mode = JSONObject.parseObject(ret, new TypeReference<OrderMode<QueryOrderModel>>() {});
return mode;
}
2. 请求测试
在平台页面也可以查看到刚刚生成的一条订单记录
3.3 关闭订单
关闭订单代码
public OrderMode closeOrder(String tradeNo) {
Map<String, Object> params = new HashMap<>();
params.put("app_id", applId);
params.put("out_trade_no", tradeNo);
// 生成签名
String sign = SignUtil.getSign(params, null, secretKey);
params.put("sign", sign);
// 发送请求
String ret = HttpUtil.sendPost(baseUrl + "/close.html", null, params);
if (StringUtils.isEmpty(ret)) {
throw new OrderException("网络忙,请稍候再试");
}
OrderMode orderMode = JSON.parseObject(ret, OrderMode.class);
return orderMode;
}
请求测试
再次查询订单
3.4 申请退款
创建订单
查看订单状态
使用草料二维码将创建订单生成的链接转换为二维码,微信扫码支付
付款成功回调接口
public Map<String, String> payNotify(HttpServletRequest request) throws UnsupportedEncodingException {
Map<String, Object> notifyDto = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String[] paramValues = request.getParameterValues(paramName);
if (paramValues != null && paramValues.length > 0) {
String paramValue = URLDecoder.decode(paramValues[0], "utf-8");
notifyDto.put(paramName, paramValue);
}
}
// 验证签名
log.info("回调:{}", notifyDto);
String sign = SignUtil.getSign(notifyDto, Arrays.asList("sign"), secretKey);
log.info("生成签名:{}", sign);
if (!sign.equals(notifyDto.get("sign"))) {
Map<String, String> ret = new HashMap<>();
ret.put("code", "FAIL");
ret.put("message", "签名错误");
return ret;
}
Map<String, String> ret = new HashMap<>();
ret.put("code", "SUCCESS");
return ret;
}
再次查看订单状态
申请退款代码
public OrderMode<RefundOrderModel> refundOrder(RefundOrderDto orderDto) {
Map<String, Object> params = new HashMap<>();
params.put("app_id", applId);
params.put("out_trade_no", orderDto.getOutTradeNo());
params.put("out_refund_no", UUID.randomUUID().toString().replaceAll("-", "").toUpperCase());
int i = new BigDecimal(orderDto.getRefundAmount()).multiply(new BigDecimal(100)).intValue();
params.put("refund_amount", i);
params.put("refund_reason", orderDto.getRefundReason());
// 生成签名
String sign = SignUtil.getSign(params, null, secretKey);
params.put("sign", sign);
log.info("关闭订单开始请求:{}", params);
String ret = HttpUtil.sendPost(baseUrl + "/refund.html", null, params);
if (StringUtils.isEmpty(ret)) {
throw new OrderException("网络忙,请稍候再试");
}
OrderMode<RefundOrderModel> mode = JSONObject.parseObject(ret, new TypeReference<OrderMode<RefundOrderModel>>() {});
return mode;
}
申请退款测试
微信收到退款
3.5 退款查询
退款查询代码
public OrderMode<QueryRefundOrderModel> queryRefund(String param) {
JSONObject jsonObject = JSON.parseObject(param);
String outRefundNo = jsonObject.getString("out_refund_no");
String refundNo = jsonObject.getString("refund_no");
Map<String, Object> params = new HashMap<>();
params.put("app_id", applId);
params.put("out_refund_no", outRefundNo);
params.put("refund_no", refundNo);
// 获取签名
String sign = SignUtil.getSign(params, null, secretKey);
params.put("sign", sign);
// 开始请求
String ret = HttpUtil.sendPost(baseUrl + "/refundid.html", null, params);
if (StringUtils.isEmpty(ret)) {
throw new OrderException("网络忙,请稍候再试");
}
// 退款信息中包含 URLEncoder 加密的中文,使用 URLDecoder 解密
try {
ret = URLDecoder.decode(ret, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
OrderMode<QueryRefundOrderModel> mode = JSONObject.parseObject(ret, new TypeReference<OrderMode<QueryRefundOrderModel>>() {});
return mode;
}
退款查询测试