遵循:BY-SA
署名-相同方式共享 4.0协议
作者:谭东
时间:2016年10月28日
环境:Windows 7
Android版微信支付官方文档和Demo问题很多,官方也没有及时更新和细化开发集成文档。
这里分享我集成Android客户端微信支付的思路和部分代码。希望对大家有帮助。
遇到的问题无非以下几种:
1、提示签名不对;
2、打包签名后的APK无法调起微信支付客户端,直接返回回调页;
3、不支持中文的Body;
4、支付的钱倍数不对,因为微信的单位是分;
... ...
首先,本文针对的是最新版微信Android支付SDK3.1.1写的,大家可以放心使用。
首先,把WXPayEntryActivity.java复制到我们的包名下的.wxapi目录下,要一致。
WXPayEntryActivity.java官方Demo里有,我这里也复制一份我的,仅供参考。
public class WXPayEntryActivity extends BaseActivity implements IWXAPIEventHandler {
private IWXAPI api;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
api = WXAPIFactory.createWXAPI(this, Conf.APP_ID);
api.handleIntent(getIntent(), this);
getSupportActionBar().setIcon(R.drawable.logo_gray);
getSupportActionBar().setTitle("购买积分");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Drawable dw = getResources().getDrawable(R.drawable.color_bg);
getSupportActionBar().setBackgroundDrawable(dw);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
this.finish();
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
api.handleIntent(intent, this);
}
@Override
public void onReq(BaseReq baseReq) {
}
@Override
public void onResp(BaseResp baseResp) {
if (baseResp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {
Log.d("info", "onPayFinish,errCode=" + baseResp.errCode);
if (baseResp.errCode == 0) {
ToastUtil.showToast(this, "支付成功");
Intent intent = new Intent("ACTION_PAY");
sendBroadcast(intent);
this.finish();
} else if (baseResp.errCode == -1) {
ToastUtil.showToast(this, "配置错误");
this.finish();
} else if (baseResp.errCode == -2) {
ToastUtil.showToast(this, "用户取消");
this.finish();
}
} else {
ToastUtil.showToast(this, baseResp.errStr);
}
}
}
这里说一下Android微信支付流程,这里本地客户端都处理了所有的流程。
客户端把所有需要的信息拼接为XML后,统一下单,发送给微信服务器API请求,获取订单id,也就是prepayId。获取到这个prepayId后,我们再把必要的数据字段sign签名MD5后,调起微信支付客户端就可以了。比支付宝麻烦很多。
1、在程序的启动页,例如欢迎页就可以注册微信API的ID了。
IWXAPI msgApi = WXAPIFactory.createWXAPI(this, Conf.APP_ID);
msgApi.registerApp(Conf.APP_ID);
2、接下来处理微信POST请求的必要参数的拼接和加密等处理。写PayActivity.java
统一下单获取prepayId的接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
微信文档地址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
把必填的参数处理下:
nonce_str:随机字符串,不长于32位。
out_trade_no:商户系统内部的订单号,32个字符内、可包含字母。
spbill_create_ip:用户端实际ip,16位,可写固定值。
notify_url:暂时写成固定的,http://www.weixin.qq.com/wxpay/pay.php。
最麻烦的,sign:32位签名,参照签名生成算法。
以上这些算法我都会写成工具类,供大家参考。
签名算法:
附上整个支付代码:
PayActivity.java里关于支付的代码:
private IWXAPI api;
private String stringA;
private String noneString;
private String out_trade_no;
private String sign;
private String prepayId;
private String ip;
private String body = "商品-积分充值";
private static final String order_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//POST请求统一下单接口地址
private String notify_url = "http://www.weixin.qq.com/wxpay/pay.php";
private String entity;//XML形式的post请求实体
private void payWeixin(int money) {
api = WXAPIFactory.createWXAPI(this, Conf.APP_ID);
try {
body = new String(body.getBytes(), "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
noneString = Utils.createRandomString(true, 32);//32位随机字符串
out_trade_no = Utils.createRandomString(true, 20) + Utils.timeStamp();//30位内部随机订单号
ip = "123.123.123.123";
stringA = "appid=" + Conf.APP_ID + "&body=" + body + "&mch_id=" + Conf.MCH_ID + "&nonce_str=" + noneString + "¬ify_url=" + notify_url + "&out_trade_no=" + out_trade_no + "&spbill_create_ip=" + ip + "&" +
"total_fee=" + money + "&trade_type=APP";
String stringSignTemp = stringA + "&key=" + Conf.KEY;
sign = Utils.getMD5(stringSignTemp).toUpperCase(Locale.getDefault());
entity = "" + Conf.APP_ID + " " + Conf.MCH_ID + " " + noneString + " " + sign +
" " + body + "" + out_trade_no + " " + money +
" " + ip + " " + notify_url + " APP ";
try {
entity = new String(entity.getBytes(), "ISO8859-1");//想要支持中文的Boby,那就要把XML转码为ISO8859-1即可
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] buf = Utils.httpPost(order_url, entity);
Message message = new Message();
message.what = 0;
message.obj = buf;
handler.sendMessage(message);
} catch (Exception e) {
Log.e("info", "异常:" + e.getMessage());
}
}
}).start();
}
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
byte[] buf = (byte[]) msg.obj;
if (buf != null && buf.length > 0) {
String content = new String(buf);
OrderResult orderResult = Tools.parseXml(new ByteArrayInputStream(content.getBytes()));
if (!TextUtils.equals(orderResult.getReturnCode(), "SUCCESS")) {
Toast.makeText(PayActivity.this, orderResult.getReturnMsg(), Toast.LENGTH_SHORT).show();
return;
}
if (!TextUtils.equals(orderResult.getResultCode(), "SUCCESS")) {
Toast.makeText(PayActivity.this, orderResult.getErrorDesc(), Toast.LENGTH_SHORT).show();
return;
}
PayReq req = new PayReq();
req.appId = Conf.APP_ID;
req.partnerId = Conf.MCH_ID;
req.prepayId = orderResult.getPrepayId();
req.packageValue = "Sign=WXPay";
req.nonceStr = noneString;
String timeStamp = Utils.timeStamp();
req.timeStamp = timeStamp;
req.sign = Utils.getMD5("appid=" + Conf.APP_ID + "&noncestr=" + noneString + "&package=Sign=WXPay" +
"&partnerid=" + Conf.MCH_ID + "&prepayid=" + orderResult.getPrepayId() + "×tamp=" + timeStamp + "&key=" + Conf.KEY).toUpperCase(Locale.getDefault());
api.sendReq(req);
} else {
Log.d("PAY_GET", "服务器请求错误");
Toast.makeText(PayActivity.this, "服务器请求错误", Toast.LENGTH_SHORT).show();
}
break;
}
}
};
public class Utils {
/**
* 产生随机字符串
*
* @param numberFlag 是否允许有字母
* @param length 随机字符串长度
* @return 随机字符串
*/
public static String createRandomString(boolean numberFlag, int length) {
String retStr = "";
String strTable = numberFlag ? "1234567890" : "1234567890abcdefghijkmnpqrstuvwxyz";
int len = strTable.length();
boolean bDone = true;
do {
retStr = "";
int count = 0;
for (int i = 0; i < length; i++) {
double dblR = Math.random() * len;
int intR = (int) Math.floor(dblR);
char c = strTable.charAt(intR);
if (('0' <= c) && (c <= '9')) {
count++;
}
retStr += strTable.charAt(intR);
}
if (count >= 2) {
bDone = false;
}
} while (bDone);
return retStr;
}
/**
* 获取MD5加密后的字符串
*
* @param val 待加密字符串
* @return 加密后字符串
*/
public static String getMD5(String val) {
byte[] m = new byte[0];
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(val.getBytes());
m = md5.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return getString(m);
}
private static String getString(byte[] b) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < b.length; i++) {
sb.append(b[i]);
}
return sb.toString();
}
/**
* 获取10位长度的时间戳
*
* @return 10位时间戳
*/
public static String timeStamp() {
String time = String.valueOf(System.currentTimeMillis());
return time.substring(0, 10);
}
public static byte[] httpGet(final String url) {
if (url == null || url.length() == 0) {
return null;
}
HttpClient httpClient = getNewHttpClient();
HttpGet httpGet = new HttpGet(url);
try {
HttpResponse resp = httpClient.execute(httpGet);
if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
return null;
}
return EntityUtils.toByteArray(resp.getEntity());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static byte[] httpPost(String url, String entity) {
if (url == null || url.length() == 0) {
return null;
}
HttpClient httpClient = getNewHttpClient();
HttpPost httpPost = new HttpPost(url);
try {
httpPost.setEntity(new StringEntity(entity));
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Content-type", "application/json");
HttpResponse resp = httpClient.execute(httpPost);
if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
return null;
}
return EntityUtils.toByteArray(resp.getEntity());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static HttpClient getNewHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new SSLSocketFactoryEx(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
return new DefaultHttpClient(ccm, params);
} catch (Exception e) {
return new DefaultHttpClient();
}
}
public static OrderResult parseXml(InputStream is) {
XmlPullParser parser = Xml.newPullParser();
OrderResult orderResult = null;
try {
parser.setInput(is, "UTF-8");
int type = parser.getEventType();
while (type != XmlPullParser.END_DOCUMENT) {
switch (type) {
case XmlPullParser.START_DOCUMENT:
break;
case XmlPullParser.START_TAG:
if (parser.getName().equals("xml")) {
orderResult = new OrderResult();
} else if (parser.getName().equals("return_code")) {
orderResult.setReturnCode(parser.nextText());
} else if (parser.getName().equals("return_msg")) {
orderResult.setReturnMsg(parser.nextText());
} else if (parser.getName().equals("result_code")) {
orderResult.setResultCode(parser.nextText());
} else if (parser.getName().equals("err_code_des")) {
orderResult.setErrorDesc(parser.nextText());
} else if (parser.getName().equals("prepay_id")) {
orderResult.setPrepayId(parser.nextText());
} else if (parser.getName().equals("sign")) {
orderResult.setSign(parser.nextText());
}
break;
case XmlPullParser.END_TAG:
break;
}
type = parser.next();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return orderResult;
}
}