支付涉及的3个重要数据:订单数据;用户账号,密码数据;电商服务器的支付宝账号
支付宝支付和微信支付区别:
微信:
网络请求:Post
数据格式:xml
签名算法:MD5
支付流程:商户服务器需要将订单信息通过统一下单api到微信服务器请求预支付交易单
异步通知处理:通知频率不一样,如果异步通知的商户服务器没有给微信服务端有效反馈,微信服务端会以一定频率重发通知
(通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
支付结果校验:提供主动查询订单支付信息接口
支付宝:
网络请求方式:get
数据格式:json
签名算法:RSA(最难破解的加密方式.)
支付流程:无需请求预支付交易单
异步通知处理:通知频率不一样,(通知频率为2m,10m,10m,1h,2h,6h,15h)
支付结果校验:不提供主动查询订单支付信息接口
支付宝开放平台:https://openhome.alipay.com/platform/home.htm下载SDK,这些活就是公司做;https://doc.open.alipay.com/doc2/detail.htm?treeId=54&articleId=104509&docType=1
//这是个调起支付宝的Demo,做了服务端和客户端的功能。但是开发中客户端请求后台是不需要做太多东西的,客户端只需要请求拿到支付宝完整的订单信息。然后直接调起支付宝即可。其他消息都是服务器做的。
把解压出的SDK文件里的资源拷贝到自己的项目里的libs
添加权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permissionandroid:name="android.permission.READ_PHONE_STATE" />
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
添加混淆规则:在proguard-project.txt里添加以下相关规则:
-libraryjarslibs/alipaySDK-20150602.jar
-keep class com.alipay.android.app.IAlixPay{*;}
-keep class com.alipay.android.app.IAlixPay$Stub{*;}
-keep class com.alipay.android.app.IRemoteServiceCallback{*;}
-keep class com.alipay.android.app.IRemoteServiceCallback$Stub{*;}
-keep class com.alipay.sdk.app.PayTask{ public *;}
-keep class com.alipay.sdk.app.AuthTask{ public *;}
//这样集成没有开发文档那样能一步一步走逻辑。这只是一个偷懒的途径,开发者可以集成结束后自行走通逻辑。有注释。希望能帮到大家。
//首先是Activity的布局
(里面的图片我就不存放了。上面是个支付宝图片,下面是个fragmet,fragment里面的布局是基本的订单信息,开发者自行布局即可)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <ImageView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@drawable/msp_demo_title_bg" android:scaleType="center" android:src="@drawable/msp_demo_title" tools:ignore="ContentDescription" /> <fragment android:id="@+id/fragment" android:name="com.example.administrator.paydemo.ExternalFragment" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>//开始做主要逻辑。调起支付宝。签名。创建订单号(下面的合作身份id,收款地址,商户私钥没有填。开发者自行添加)
public class MainActivity extends AppCompatActivity { //开发者的PID //合作身份者id,以2088开头的16位纯数字 public static final String PARTNER = ""; //收款地址 //收款支付宝账号 此处的收款账号是与支付宝签约时填写的账号 public static final String SELLER = ""; //商户私钥 // 商户私钥,pkcs8格式 public static final String RSA_PRIVATE = ""; //支付宝公钥 public static final String RSA_PUBLIC = ""; private static final int SDK_PAY_FLAG = 1; private Button pay_button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pay_button = (Button) findViewById(R.id.pay); pay_button.setOnClickListener(new View.OnClickListener() { private String sign; @Override public void onClick(View view) { if (TextUtils.isEmpty(PARTNER) || TextUtils.isEmpty(RSA_PRIVATE) || TextUtils.isEmpty(SELLER)) { new AlertDialog.Builder(MainActivity.this).setTitle("警告").setMessage("需要配置PARTNER | RSA_PRIVATE| SELLER") .setPositiveButton("确定", new AlertDialog.OnClickListener() { public void onClick(DialogInterface dialoginterface, int i) { finish(); } }).show(); return; } String orderInfo = getOrderInfo("测试的商品", "该测试商品的详细描述", "0.01"); Log.i("siginsnns", orderInfo + ""); /** * 特别注意,这里的签名逻辑需要放在服务端,切勿将私钥泄露在代码中! */ //签名错误 sign = sign(orderInfo); Log.e("sign", sign + ""); try { /** * 仅需对sign 做URL编码 */ sign = URLEncoder.encode(sign, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } /** * 完整的符合支付宝参数规范的订单信息 */ final String payInfo = orderInfo + "&sign=\"" + sign + "\"&" + getSignType(); Runnable payRunnable = new Runnable() { @Override public void run() { // 构造PayTask 对象 PayTask alipay = new PayTask(MainActivity.this); // 调用支付接口,获取支付结果 String result = alipay.pay(payInfo, true); Message msg = new Message(); msg.what = SDK_PAY_FLAG; msg.obj = result; mHandler.sendMessage(msg); } }; // 必须异步调用 Thread payThread = new Thread(payRunnable); payThread.start(); } }); } //创建一个对数据订单的创建 public static String getOrderInfo(String subject, String body, String price) { // 签约合作者身份ID String orderInfo = "partner=" + "\"" + PARTNER + "\""; // 签约卖家支付宝账号 orderInfo += "&seller_id=" + "\"" + SELLER + "\""; //商户网站唯一订单号 orderInfo += "&out_trade_no=" + "\"" + getOutTradeNo() + "\""; //商品的名称 orderInfo += "&subject=" + "\"" + subject + "\""; //支付的详情 orderInfo += "&body=" + "\"" + body + "\""; //支付的总金额 orderInfo += "&total_fee=" + "\"" + price + "\""; //服务器异步通知页面路径 orderInfo += "¬ify_url=" + "\"" + "http://notify.msp.hk/notify.htm" + "\""; // 服务接口名称, 固定值 orderInfo += "&service=\"mobile.securitypay.pay\""; //支付的类型 orderInfo += "&payment_type=\"1\""; // 参数编码, 固定值 orderInfo += "&_input_charset=\"utf-8\""; // 设置未付款交易的超时时间 // 默认30分钟,一旦超时,该笔交易就会自动被关闭。 // 取值范围:1m~15d。 // m-分钟,h-小时,d-天,1c-当天(无论交易何时创建,都在0点关闭)。 // 该参数数值不接受小数点,如1.5h,可转换为90m。 orderInfo += "&it_b_pay=\"30m\""; // 支付宝处理完请求后,当前页面跳转到商户指定页面的路径,可空 orderInfo += "&return_url=\"m.alipay.com\""; return orderInfo; } /** * get the out_trade_no for an order. 生成商户订单号,该值在商户端应保持唯一(可自定义格式规范) */ private static String getOutTradeNo() { SimpleDateFormat format = new SimpleDateFormat("MMddHHmmss", Locale.getDefault()); Date date = new Date(); String key = format.format(date); Random r = new Random(); key = key + r.nextInt(); key = key.substring(0, 15); return key; } //endregion /** * sign the order info. 对订单信息进行签名 * * @param content 待签名订单信息 */ public static String sign(String content) { return SignUtils.sign(content, RSA_PRIVATE); } /** * get the sign type we use. 获取签名方式 */ private String getSignType() { return "sign_type=\"RSA\""; } /** * call alipay sdk pay. 调用SDK支付+签名 */ @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @SuppressWarnings("unused") public void handleMessage(Message msg) { switch (msg.what) { case SDK_PAY_FLAG: { PayResult payResult = new PayResult((String) msg.obj); /** * 同步返回的结果必须放置到服务端进行验证(验证的规则请看https://doc.open.alipay.com/doc2/ * detail.htm?spm=0.0.0.0.xdvAU6&treeId=59&articleId=103665& * docType=1) 建议商户依赖异步通知 */ String resultInfo = payResult.getResult();// 同步返回需要验证的信息 String resultStatus = payResult.getResultStatus(); // 判断resultStatus 为“9000”则代表支付成功,具体状态码代表含义可参考接口文档 if (TextUtils.equals(resultStatus, "9000")) { Toast.makeText(MainActivity.this, "支付成功", Toast.LENGTH_SHORT).show(); } else { // 判断resultStatus 为非"9000"则代表可能支付失败 // "8000"代表支付结果因为支付渠道原因或者系统原因还在等待支付结果确认,最终交易是否成功以服务端异步通知为准(小概率状态) if (TextUtils.equals(resultStatus, "8000")) { Toast.makeText(MainActivity.this, "支付结果确认中", Toast.LENGTH_SHORT).show(); } else { // 其他值就可以判断为支付失败,包括用户主动取消支付,或者系统返回的错误 Toast.makeText(MainActivity.this, "支付失败", Toast.LENGTH_SHORT).show(); } } break; } default: break; } } }; /** * get the sdk version. 获取SDK版本号 */ public void getSDKVersion() { PayTask payTask = new PayTask(this); String version = payTask.getVersion(); Toast.makeText(this, version, Toast.LENGTH_SHORT).show(); } }//对订单信息进行签名的工具类
//对订单信息进行签名的工具类 public class SignUtils { private static final String ALGORITHM = "RSA"; private static final String SIGN_ALGORITHMS = "SHA1WithRSA"; private static final String DEFAULT_CHARSET = "UTF-8"; public static String sign(String content, String privateKey) { try { PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec( Base64.decode(privateKey)); KeyFactory keyf = KeyFactory.getInstance(ALGORITHM); PrivateKey priKey = keyf.generatePrivate(priPKCS8); java.security.Signature signature = java.security.Signature .getInstance(SIGN_ALGORITHMS); signature.initSign(priKey); signature.update(content.getBytes(DEFAULT_CHARSET)); byte[] signed = signature.sign(); return Base64.encode(signed); } catch (Exception e) { e.printStackTrace(); } return null; } }//Base64加密算法。讲签名进行加密
public final class Base64 { private static final int BASELENGTH = 128; private static final int LOOKUPLENGTH = 64; private static final int TWENTYFOURBITGROUP = 24; private static final int EIGHTBIT = 8; private static final int SIXTEENBIT = 16; private static final int FOURBYTE = 4; private static final int SIGN = -128; private static char PAD = '='; private static byte[] base64Alphabet = new byte[BASELENGTH]; private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; static { for (int i = 0; i < BASELENGTH; ++i) { base64Alphabet[i] = -1; } for (int i = 'Z'; i >= 'A'; i--) { base64Alphabet[i] = (byte) (i - 'A'); } for (int i = 'z'; i >= 'a'; i--) { base64Alphabet[i] = (byte) (i - 'a' + 26); } for (int i = '9'; i >= '0'; i--) { base64Alphabet[i] = (byte) (i - '0' + 52); } base64Alphabet['+'] = 62; base64Alphabet['/'] = 63; for (int i = 0; i <= 25; i++) { lookUpBase64Alphabet[i] = (char) ('A' + i); } for (int i = 26, j = 0; i <= 51; i++, j++) { lookUpBase64Alphabet[i] = (char) ('a' + j); } for (int i = 52, j = 0; i <= 61; i++, j++) { lookUpBase64Alphabet[i] = (char) ('0' + j); } lookUpBase64Alphabet[62] = (char) '+'; lookUpBase64Alphabet[63] = (char) '/'; } private static boolean isWhiteSpace(char octect) { return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); } private static boolean isPad(char octect) { return (octect == PAD); } private static boolean isData(char octect) { return (octect < BASELENGTH && base64Alphabet[octect] != -1); } /** * Encodes hex octects into Base64 * * @param binaryData * Array containing binaryData * @return Encoded Base64 array */ public static String encode(byte[] binaryData) { if (binaryData == null) { return null; } int lengthDataBits = binaryData.length * EIGHTBIT; if (lengthDataBits == 0) { return ""; } int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; char encodedData[] = null; encodedData = new char[numberQuartet * 4]; byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; int encodedIndex = 0; int dataIndex = 0; for (int i = 0; i < numberTriplets; i++) { b1 = binaryData[dataIndex++]; b2 = binaryData[dataIndex++]; b3 = binaryData[dataIndex++]; l = (byte) (b2 & 0x0f); k = (byte) (b1 & 0x03); byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; } // form integral number of 6-bit groups if (fewerThan24bits == EIGHTBIT) { b1 = binaryData[dataIndex]; k = (byte) (b1 & 0x03); byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; encodedData[encodedIndex++] = PAD; encodedData[encodedIndex++] = PAD; } else if (fewerThan24bits == SIXTEENBIT) { b1 = binaryData[dataIndex]; b2 = binaryData[dataIndex + 1]; l = (byte) (b2 & 0x0f); k = (byte) (b1 & 0x03); byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; encodedData[encodedIndex++] = PAD; } return new String(encodedData); } /** * Decodes Base64 data into octects * * @param encoded * string containing Base64 data * @return Array containind decoded data. */ public static byte[] decode(String encoded) { if (encoded == null) { return null; } char[] base64Data = encoded.toCharArray(); // remove white spaces int len = removeWhiteSpace(base64Data); if (len % FOURBYTE != 0) { return null;// should be divisible by four } int numberQuadruple = (len / FOURBYTE); if (numberQuadruple == 0) { return new byte[0]; } byte decodedData[] = null; byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; char d1 = 0, d2 = 0, d3 = 0, d4 = 0; int i = 0; int encodedIndex = 0; int dataIndex = 0; decodedData = new byte[(numberQuadruple) * 3]; for (; i < numberQuadruple - 1; i++) { if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) { return null; }// if found "no data" just return null b1 = base64Alphabet[d1]; b2 = base64Alphabet[d2]; b3 = base64Alphabet[d3]; b4 = base64Alphabet[d4]; decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); } if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { return null;// if found "no data" just return null } b1 = base64Alphabet[d1]; b2 = base64Alphabet[d2]; d3 = base64Data[dataIndex++]; d4 = base64Data[dataIndex++]; if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters if (isPad(d3) && isPad(d4)) { if ((b2 & 0xf) != 0)// last 4 bits should be zero { return null; } byte[] tmp = new byte[i * 3 + 1]; System.arraycopy(decodedData, 0, tmp, 0, i * 3); tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); return tmp; } else if (!isPad(d3) && isPad(d4)) { b3 = base64Alphabet[d3]; if ((b3 & 0x3) != 0)// last 2 bits should be zero { return null; } byte[] tmp = new byte[i * 3 + 2]; System.arraycopy(decodedData, 0, tmp, 0, i * 3); tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); return tmp; } else { return null; } } else { // No PAD e.g 3cQl b3 = base64Alphabet[d3]; b4 = base64Alphabet[d4]; decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); } return decodedData; } /** * remove WhiteSpace from MIME containing encoded Base64 data. * * @param data * the byte array of base64 data (with WS) * @return the new length */ private static int removeWhiteSpace(char[] data) { if (data == null) { return 0; } // count characters that's not whitespace int newSize = 0; int len = data.length; for (int i = 0; i < len; i++) { if (!isWhiteSpace(data[i])) { data[newSize++] = data[i]; } } return newSize; } }结果验证的工具类
public class PayResult { private String resultStatus; private String result; private String memo; public PayResult(String rawResult) { if (TextUtils.isEmpty(rawResult)) return; String[] resultParams = rawResult.split(";"); for (String resultParam : resultParams) { if (resultParam.startsWith("resultStatus")) { resultStatus = gatValue(resultParam, "resultStatus"); } if (resultParam.startsWith("result")) { result = gatValue(resultParam, "result"); } if (resultParam.startsWith("memo")) { memo = gatValue(resultParam, "memo"); } } } @Override public String toString() { return "resultStatus={" + resultStatus + "};memo={" + memo + "};result={" + result + "}"; } private String gatValue(String content, String key) { String prefix = key + "={"; return content.substring(content.indexOf(prefix) + prefix.length(), content.lastIndexOf("}")); } /** * @return the resultStatus */ public String getResultStatus() { return resultStatus; } /** * @return the memo */ public String getMemo() { return memo; } /** * @return the result */ public String getResult() { return result; } }//这个fragment只是填充了布局,里面的布局是商品信息,逻辑都在Actiity。
public class ExternalFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.pay_external, null); return view; } }
//fragment的布局。这些按钮都是在Activity做的操作。
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" ><LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center_vertical" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="商品名称:" android:textColor="#777777" tools:ignore="HardcodedText" /> <TextView android:id="@+id/product_subject" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="right" android:text="测试的商品" android:textColor="#333333" tools:ignore="HardcodedText" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center_vertical" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="商品描述:" android:textColor="#777777" tools:ignore="HardcodedText" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="right" android:text="该测试商品的详细描述" android:textColor="#333333" tools:ignore="HardcodedText" android:id="@+id/test_desc" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center_vertical" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="商品价格:" android:textColor="#777777" tools:ignore="HardcodedText" /> <TextView android:id="@+id/product_price" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="right" android:text="0.01元" android:textColor="#ff6600" tools:ignore="HardcodedText" /> </LinearLayout> <Button android:id="@+id/pay" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:onClick="pay" android:text="支付" tools:ignore="HardcodedText" android:background="@drawable/log_r_shape" /> </LinearLayout> </ScrollView>