背景:话费流量充值原本有一套供客户端调用进行话费流量充值的接口,具体交互流程简图如下:

客户端提交充值的手机号码,服务端返回相对应的充值产品列表,客户端再次提交所选择的充值产品并提交订单,服务端返回订单支付信息并调起支付客户端,用户完成支付,服务端收到支付的回调后再次对支付服务端发起一次查询,确认订单信息是否存在,确认存后,充值服务端调用CP下订单接口进行下单和充值产品发放。 一切正常运行,但突然有一天,产品说要做一套不用支付流程的充值接口,一但调用下单接口且传参数校验通过,立即发放话费和流量。具体的需求场景是:用户通过参加系统某些业务如应用中心或生活服务,所做的活动,获得了100元话费,这时只需要用户提交他的手机号码,即可以实现这100元话费的发放。于是就跟开发同学商量了下,觉得在原有的接口上改造风险太大,会危及原有的充值业务,最后决定重新做一套独立的,供接入方业务的服务端调用的充值接口,即接入的业务的服务端接收到客户端传上来的手机号码,就调用这套接口对该号码进行话费或者流量的充值,不需要走支付流程。
接口的测试:
新接口开发完成并提测的时候,并没有业务接入,因此无法通过客户端对接口交进行测试,需要单独写相应的脚本进行测试。其实,原来的充值接口,即走支付流程的旧充值接口(暂称为旧充值接口,下同),有一个一直都在使用和维护的,基于HttpClient的java测试脚本,能脱离充值的客户端,单独对旧的充值接口进行测试,但由于新充值接口不走账户token和支付信息等校验,旧脚本不适用,所以重新写了新接口的测试脚本。 其实对于接口的测试,除了java脚本,也尝试过用python或者Jmeter等方式,但由于开发的代码是java,一些参数加密解密等算法可以直接拿过来用这个得天独厚的优势,所以毅然决然的选择了java脚本。 以下,以新充值接口的预查询接口为例,其他接口均以类似的方法构造。 预查询接口,是充值前查询所提交的手机号码与相应的产品id是否能够充值,具体详情如下:


首先是封装请求的header:
public class ReqHeader {
private String version;
private String channelId;
private String timestamp;
private String seqNo;
private String signType;
private String signNonce;
private String sign;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getSeqNo() {
return seqNo;
}
public void setSeqNo(String seqNo) {
this.seqNo = seqNo;
}
public String getSignType() {
return signType;
}
public void setSignType(String signType) {
this.signType = signType;
}
public String getSignNonce() {
return signNonce;
}
public void setSignNonce(String signNonce) {
this.signNonce = signNonce;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
}
接着封装预查询接口请求的body:
public class PreOrderBody {
private String phone;
private String type;
private String productIds;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getProductIds() {
return productIds;
}
public void setProductIds(String productIds) {
this.productIds = productIds;
}
}
Header参数塞值:
public static ReqHeader buildReqHeader(){
ReqHeader reqHeader = new ReqHeader();
reqHeader.setVersion(version);
reqHeader.setChannelId(channelId);
reqHeader.setTimestamp("" + System.currentTimeMillis());
reqHeader.setSeqNo(SeqNo);
reqHeader.setSignType(signType);
reqHeader.setSignNonce(SignNonce);
return reqHeader;
}
有没有发现:
header的参数少了sign的值?对于sign的值,开发的接口文档里面写的是:“sign: header及body中所有值为非空字段均需参与签名(值为null或空字符串都不需要参与签名),按变量名英文单词排序组成signBase=field1=ksks&….&fieldn=ksksk,然后MD5(signBase+”&key=”+appKey)”,即:所有非空参数组装+appkey(单独给接入方的key)组装起来,再取MD5,得到sign的值,因此,需要基于body和header的参数,单独构造sign:
public class ParserObjectUtils {
public static Map<String, String> parseObjectField(Object object) {
Map<String, String> fieldMap = new HashMap<String, String>();
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
try {
String fieldName = field.getName();
field.setAccessible(true);
Object value = field.get(object);
if (value instanceof String) {
fieldMap.put(fieldName, (String) value);
}
} catch (Exception e) {
}
}
return fieldMap;
}
public static final String makeSignBaseWithoutNullValue(Map<String, String> params, String[] withoutFields) {
// 不参与签名
for (String without : withoutFields) {
params.remove(without);
}
// 过滤重复key
List<String> keys = new ArrayList<String>(params.keySet());
// 按照key ascii排序
Collections.sort(keys);
// key1=value1&=key2=value2组装待签名原始参数
StringBuilder base = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = (String) keys.get(i);
String value = (String) params.get(key);
// 值为空时不参考签名.
if (value== null || value.length()==0) {
continue;
}
base.append(key).append("=").append(value).append("&");
}
if (base.length() > 1) {
base.deleteCharAt(base.length() - 1);
}
return base.toString();
}
}
取MD5值:
public class MD5Util {
public static String md5Encode(String inStr) {
System.out.println("MD5前:" + inStr);
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
byte[] byteArray = new byte[0];
try {
byteArray = inStr.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
System.out.println("md5后:" + hexValue);
return hexValue.toString();
}
签名sign:
//签名Sign
public static String getSign(ReqHeader reqHeader, Object body){
Map<String, String> paramsMap = ParserObjectUtils.parseObjectField(reqHeader);
paramsMap.putAll(ParserObjectUtils.parseObjectField(body));
String signBase = ParserObjectUtils.makeSignBaseWithoutNullValue(paramsMap, new String []{"sign"});
return MD5Util.md5Encode(signBase + "&key=" + channelMd5Key);
}
组装预查询接口:
public class PreOrderReq {
private ReqHeader header;
private PreOrderBody body;
public ReqHeader getHeader() {
return header;
}
public void setHeader(ReqHeader header) {
this.header = header;
}
public PreOrderBody getBody() {
return body;
}
public void setBody(PreOrderBody body) {
this.body = body;
}
}
塞入header和body,以及body的参数和sign:
public static PreOrderReq generatePreOrderReq(){
ReqHeader reqHeader = buildReqHeader();
PreOrderBody preOrderBody = new PreOrderBody();
preOrderBody.setPhone(phone);
preOrderBody.setType(type);
preOrderBody.setProductIds(productIds);
reqHeader.setSign(getSign(reqHeader, preOrderBody));
PreOrderReq preOrderReq = new PreOrderReq();
preOrderReq.setHeader(reqHeader);
preOrderReq.setBody(preOrderBody);
return preOrderReq;
}
将preOrderReq转换为JSONString,然后通过HttpClient进行请求:
public class RechargeServerApiTestCase extends JsonTest {
@Test
public void JUnitTest() throws Exception {
PreOrderReq preOrderReq = JsonTest.generatePreOrderReq();
String url_preOrder = "https://recharge.meizu.com/service/preorder";
httpPostWithJSON(url_preOrder, JSON.toJSONString(preOrderReq));
}
}
private static final String APPLICATION_JSON = "application/json";
private static final String CONTENT_TYPE_TEXT_JSON = "text/json";
public static void httpPostWithJSON(String url, String json) throws Exception {
// 将JSON进行UTF-8编码,以便传输中文
String encoderJson = URLEncoder.encode(json, HTTP.UTF_8);
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpPost httppost = new HttpPost(url);
httppost.addHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON);
StringEntity se = new StringEntity(json);
se.setContentType(CONTENT_TYPE_TEXT_JSON);
se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON));
httppost.setEntity(se);
CloseableHttpResponse response = httpclient.execute(httppost);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
System.out.println("-----------------------------------------");
System.out.println("executing request " + httppost.getURI());
String content = EntityUtils.toString(entity, "UTF-8");
System.out.println("Requset content: " + json.toString());//打印请求内容,检查组装结果是否正确
System.out.println("Response content:" + content);//打印请求返回内容
JSONObject jsonObject = JSON.parseObject(content);
System.out.println("code : " + jsonObject.getString("code"));//打印请求返回code
System.out.println("value : " + jsonObject.getString("value"));//单独取出返回value值
System.out.println("--------------------end-------------------");
}
} finally {
response.close();
}
}
下订单和订单查询接口也是类型的方法,预查询、下单、订单查询方法如下:
public class RechargeServerApiTestCase extends JsonTest {
@Test
public void JUnitTest() throws Exception {
PreOrderReq preOrderReq = JsonTest.generatePreOrderReq();
String url_preOrder = "preorder_url";
httpPostWithJSON(url_preOrder, JSON.toJSONString(preOrderReq));
CreateOrderReq createOrderReq = JsonTest.buildCreateOrderReq();
String url_CreateOrder = "create_url";
httpPostWithJSON(url_CreateOrder, JSON.toJSONString(createOrderReq));
Thread.sleep(100);
QueryOrderReq queryOrderReq =JsonTest.buildQueryOrderReq();
String url_QueryOrder = "query_url";
httpPostWithJSON(url_QueryOrder, JSON.toJSONString(queryOrderReq));
}
总结:
- 1.方法解耦、遏制重复代码;
- 2.参数和逻辑分离,以便传入不同参数值进行测试;
- 3.待补充..