2017-9月微信公众号支付-Java详解

时间:2017-09-10 14:27:17   收藏:0   阅读:2186

 微信支付源代码

在此之前,先C麻瓜藤N遍,MD官方文档一半正确一半错误。言归正传,

微信支付整体流程:微信授权登录商户的公众号——微信支付的公众号配置——统一下单——微信js调起支付页面——输入密码支付——支付成功,异步回调URL处理商户的相应业务

一、业务场景:

  先看一下支付的业务场景:用户使用微信登录商户页面,点击支付按钮,调起微信支付,选择付款卡号,输入密码,完成支付,如图:

 技术分享

 

场景十分简单,不过步骤比较多,稍不注意就掉坑里了。

二、微信公众号支付的配置准备:

1)调用公众号支付,首先你得有个公众号,并且是服务号,如果是订阅号,请联系上司改为服务号。有了服务号,你需要去申请微信支付接口,具体可参考怎么申请微信支付接口(这个过程中会要求填写一个网页授权域名,下面的配置中会用到)。然后登录微信公众平台https://mp.weixin.qq.com,配置JS接口安全域名和网页授权域名:公众号设置——功能设置——JS接口安全域名,网页授权域名(注:会下载MP_verify_8QXHzPy7cuvgQiqU.txt文件,如图,将其放到项目根目录下)

 

技术分享

两个域名配置成项目的根目录即可,

如图(注:mozhu.iok.la替换成你的网站域名,wap37lion替换成你的项目名称;开头没有http/https,结尾没有斜杠“/”),

技术分享

 

 

2)登录后,修改支付授权目录:微信支付——开发配置——支付授权目录更改。

注:支付授权目录指的是你的支付页面所在目录。如图,

技术分享

假设你的项目名为wTest2,你是在pay目录下,NewFile.html中调起的微信支付,外网访问NewFile.html的全路径为"http://你的网站域名/wTest2/pay/",那么你的目录是:"http://你的网站域名/wTest2/pay/";或者,如果你的网站域名就是你的项目根目录,外网访问NewFile.html的全路径为"http://你的网站域名/pay/",那目录就是"http://你的网站域名/pay/"。如图

技术分享

 

三、微信网页授权登录公众号

授权可参考官方文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

1)用户同意授权,获取code

在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。 

参数说明

参数是否必须说明
appid 公众号的唯一标识
redirect_uri 授权后重定向的回调链接地址,请使用urlEncode对链接进行处理
response_type 返回类型,请填写code
scope 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)
state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
#wechat_redirect 无论直接打开还是做页面302重定向时候,必须带此参数

如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE,也就是说授权成功后,微信会自动把获取到的code当作参数传到redirect_uri路径下。我们只需要定义好redirect_uri,在redirect_uri相应的控制器中获取code参数即可

示例代码(在控制器中):

package com.common;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("test/TestResource")
public class TestResource {
    @GET
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("wxLogin")//redirect_uri
    public Response wxLogin(@QueryParam("code") String code){
        System.out.println(code);
        return null;
    }
}

 

2)通过code获取access_token

 

请求方法

获取code后,请求以下链接获取access_token:  https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code 

参数说明

参数是否必须说明
appid 公众号的唯一标识
secret 公众号的appsecret
code 填写第一步获取的code参数
grant_type 填写为authorization_code   

示例代码:

public static Map<String, Object> getAccessToken(String code) {
        Map<String, Object> data = new HashMap();
        try {
            String appid="";//商户的APPID
            String appsecret="";//商户的APPSECRET
                    
            String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
                    appid, appsecret, code);
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String tokens = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(tokens,Map.class);
       

        String openId = data.get("openid")+"";
        String accessToken=data.get("access_token")+"";


  


        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

 

返回说明

正确时返回的JSON数据包如下:

{ "access_token":"ACCESS_TOKEN",    

 "expires_in":7200,    

 "refresh_token":"REFRESH_TOKEN",    

 "openid":"OPENID",    

 "scope":"SCOPE" } 

参数描述
access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
expires_in access_token接口调用凭证超时时间,单位(秒)
refresh_token 用户刷新access_token
openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
scope 用户授权的作用域,使用逗号(,)分隔

 

3)通过access_token获取用户信息

请求方法

http:GET(请使用https协议) https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 

参数说明

参数描述
access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
openid 用户的唯一标识
lang 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

示例代码:

  /**
     * 获取用户信息
     *
     * @param accessToken
     * @param openId
     * @return
     */
    public static Map<String, Object> getUserInfo(String accessToken, String openId) {
        Map<String, Object> data = new HashMap();
        String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
        try {
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String responseStr = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(responseStr,Map.class);
        } catch (Exception ex) {
        }
        return data;
    }

 

返回说明

正确时返回的JSON数据包如下:

{    "openid":" OPENID",  

 " nickname": NICKNAME,   

 "sex":"1",   

 "province":"PROVINCE"   

 "city":"CITY",   

 "country":"COUNTRY",    

 "headimgurl":    "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ

4eMsv84eavHiaiceqxibJxCfHe/46",  

"privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],    

 "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" 

参数描述
openid 用户的唯一标识
nickname 用户昵称
sex 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
province 用户个人资料填写的省份
city 普通用户个人资料填写的城市
country 国家,如中国为CN
headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
privilege 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
unionid 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。

 四、统一下单(请求参数较多,仔细看)

调用微信支付必须在后台生成统一下单接口,获取prepay_id。需要的参数,可参考官方文档统一下单。看不懂,不要急,往下看。

接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder

请求参数说明:

appid :应用ID,登陆微信公众号后台——开发——基本配置获取

 

mch_id : 微信支付商户号,登陆微信支付后台,即可看到

 

device_info:设备号,终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"

 

body:商品描述,商品或 支付单简要描述(先随便传个字符串)

 

trade_type:交易类型,取值如下:JSAPI,NATIVE,APP。我们这里使用的JSAPI。他们的区别,请参考https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_2

 

nonce_str:随机字符串,不长于32位(参考算法https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3)

 

notify_url:通知地址,接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数,不能是相对地址。

out_trade_no:商户订单号,商户系统内部的订单号,32个字符内、可包含字母(参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_2)

 

total_fee:总金额,订单总金额,单位为分

 

openid:用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。(要是不知道这个从哪里来的话,没关系。微信不是给咱写文档了吗https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_4)

还有最最重要的角色总要在最后登场。

sign:签名,官方给的签名算法https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3。

key:获取方法:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置

技术分享

 

 

示例代码:

/**
     * 微信支付 获得PrePayId
     * 
     * @param payURL
     *            调用支付的完整URL request.getRequestURL().toString();
     * @param nonce_str
     *            随机字符串 用WXPayUtil生成
     * @param body
     *            商品或支付单简要描述
     * @param out_trade_no
     *            商户系统内部的订单号,32个字符内、可包含字母 。用WXPayUtil生成
     * @param total_fee
     *            订单总金额,单位为分
     * @param IP
     *            APP和网页支付提交用户端ip
     * @param notify_url
     *            接收微信支付异步通知回调地址
     * @param openid
     *            用户openId request.getSession().getAttribute("openid")
     * @throws IOException
     */
    public Map<String, Object> wxPay(String payURL,String nonce_str, String body, String out_trade_no, String total_fee, String ip,
            String notify_url, String openId) throws Exception {
        WxPaySendDataPO result = new WxPaySendDataPO();
        Map<String,Object>map=new HashMap<String, Object>();
        
        //统一下单
        Map resultMap=WXPayUtil.unifiedorder(nonce_str, body, out_trade_no, total_fee, ip, notify_url, openId);
        String timeStamp = WXPayUtil.getTimeStamp();//时间戳
        String result_code=resultMap.get("result_code")+"";
        String return_code=resultMap.get("return_code")+"";
        if("SUCCESS".equals(return_code)&&"SUCCESS".equals(result_code)){
            //result.setResult_code(result_code);
            result.setAppid(WXUtil.APPID);
            result.setTimeStamp(timeStamp);
            result.setNonce_str(nonce_str);
            result.setPackageStr("prepay_id="+resultMap.get("prepay_id"));
            result.setSignType("MD5");
//            
//            // WeChatUtil.unifiedorder(.....) 下单操作中,也有签名操作,那个只针对统一下单,要区别于下面的paySign
//            //第二次签名,将微信返回的数据再进行签名
            SortedMap<Object,Object> signMap = new TreeMap<Object,Object>();
            signMap.put("appId", WXUtil.APPID);
            signMap.put("timeStamp", timeStamp);
            signMap.put("nonceStr", nonce_str);
            signMap.put("package", "prepay_id="+resultMap.get("prepay_id"));  //注:看清楚,值为:prepay_id=xxx,别直接放成了wxReturnData.getPrepay_id()
            signMap.put("signType", "MD5");
           String paySign = WXSignUtil.createSign(signMap,  WXUtil.KEY);//支付签名
            result.setSign(paySign);
            
            String signature=WXUtil.getSHA1Sign( payURL, timeStamp, nonce_str);
            map.put("signature",signature);
            map.put("code", 1);
            map.put("msg", "success");
            map.put("data", result);
            
        }else{
            map.put("code", 2);
            map.put("msg", "获取prepay_id失败");
            map.put("data", "");
        }
        return map;
    }

WxPaySendDataPO:

技术分享
package com.util;



public class WxPaySendDataPO {
    //公众账号ID
    private String appid;
    //附加数据
    private String attach;
    //商品描述
    private String body;
    //商户号
    private String mch_id;
    //随机字符串
    private String nonce_str;
    //通知地址
    private String notify_url;
    //商户订单号
    private String out_trade_no;
    //标价金额
    private String total_fee;
    //交易类型
    private String trade_type;
    //终端IP
    private String spbill_create_ip;
    //用户标识
    private String openid;
    //签名
    private String sign;
    //预支付id
    private String prepay_id;
    //签名类型:MD5
    private String signType;
    //时间戳
    private String timeStamp;
    //微信支付时用到的prepay_id
    private String packageStr;
    
    private String return_code;
    private String return_msg;
    private String result_code;
    
    private String bank_type;
    private Integer cash_fee;
    private String fee_type;
    private String is_subscribe;
    private String time_end;
    //微信支付订单号
    private String transaction_id;
    private String ip;
    private Integer coupon_count;
    private Integer coupon_fee;
    private Integer coupon_fee_0;
    private String coupon_type_0;
    private String coupon_id_0;
    
    public String getCoupon_type_0() {
        return coupon_type_0;
    }
    public void setCoupon_type_0(String coupon_type_0) {
        this.coupon_type_0 = coupon_type_0;
    }
    public String getCoupon_id_0() {
        return coupon_id_0;
    }
    public void setCoupon_id_0(String coupon_id_0) {
        this.coupon_id_0 = coupon_id_0;
    }
    public Integer getCoupon_fee_0() {
        return coupon_fee_0;
    }
    public void setCoupon_fee_0(Integer coupon_fee_0) {
        this.coupon_fee_0 = coupon_fee_0;
    }
    public Integer getCoupon_fee() {
        return coupon_fee;
    }
    public void setCoupon_fee(Integer coupon_fee) {
        this.coupon_fee = coupon_fee;
    }
    public Integer getCoupon_count() {
        return coupon_count;
    }
    public void setCoupon_count(Integer coupon_count) {
        this.coupon_count = coupon_count;
    }
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
    public String getBank_type() {
        return bank_type;
    }
    public void setBank_type(String bank_type) {
        this.bank_type = bank_type;
    }
    public Integer getCash_fee() {
        return cash_fee;
    }
    public void setCash_fee(Integer cash_fee) {
        this.cash_fee = cash_fee;
    }
    public String getFee_type() {
        return fee_type;
    }
    public void setFee_type(String fee_type) {
        this.fee_type = fee_type;
    }
    public String getIs_subscribe() {
        return is_subscribe;
    }
    public void setIs_subscribe(String is_subscribe) {
        this.is_subscribe = is_subscribe;
    }
    public String getTime_end() {
        return time_end;
    }
    public void setTime_end(String time_end) {
        this.time_end = time_end;
    }
    public String getTransaction_id() {
        return transaction_id;
    }
    public void setTransaction_id(String transaction_id) {
        this.transaction_id = transaction_id;
    }
    public String getAppid() {
        return appid;
    }
    public void setAppid(String appid) {
        this.appid = appid;
    }
    public String getAttach() {
        return attach;
    }
    public void setAttach(String attach) {
        this.attach = attach;
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
    public String getMch_id() {
        return mch_id;
    }
    public void setMch_id(String mch_id) {
        this.mch_id = mch_id;
    }
    public String getNonce_str() {
        return nonce_str;
    }
    public void setNonce_str(String nonce_str) {
        this.nonce_str = nonce_str;
    }
    public String getNotify_url() {
        return notify_url;
    }
    public void setNotify_url(String notify_url) {
        this.notify_url = notify_url;
    }
    public String getOut_trade_no() {
        return out_trade_no;
    }
    public void setOut_trade_no(String out_trade_no) {
        this.out_trade_no = out_trade_no;
    }
    public String getTotal_fee() {
        return total_fee;
    }
    public void setTotal_fee(String total_fee) {
        this.total_fee = total_fee;
    }
    public String getTrade_type() {
        return trade_type;
    }
    public void setTrade_type(String trade_type) {
        this.trade_type = trade_type;
    }
    public String getSpbill_create_ip() {
        return spbill_create_ip;
    }
    public void setSpbill_create_ip(String spbill_create_ip) {
        this.spbill_create_ip = spbill_create_ip;
    }
    public String getOpenid() {
        return openid;
    }
    public void setOpenid(String openid) {
        this.openid = openid;
    }
    public String getReturn_code() {
        return return_code;
    }
    public void setReturn_code(String return_code) {
        this.return_code = return_code;
    }
    public String getReturn_msg() {
        return return_msg;
    }
    public void setReturn_msg(String return_msg) {
        this.return_msg = return_msg;
    }
    public String getResult_code() {
        return result_code;
    }
    public void setResult_code(String result_code) {
        this.result_code = result_code;
    }
    public String getSign() {
        return sign;
    }
    public void setSign(String sign) {
        this.sign = sign;
    }
    public String getPrepay_id() {
        return prepay_id;
    }
    public void setPrepay_id(String prepay_id) {
        this.prepay_id = prepay_id;
    }
    public String getSignType() {
        return signType;
    }
    public void setSignType(String signType) {
        this.signType = signType;
    }
    public String getTimeStamp() {
        return timeStamp;
    }
    public void setTimeStamp(String timeStamp) {
        this.timeStamp = timeStamp;
    }
    
    public String getPackageStr() {
        return packageStr;
    }
    public void setPackageStr(String packageStr) {
        this.packageStr = packageStr;
    }
    @Override
    public String toString() {
        return "WxPaySendData [appid=" + appid + ", attach=" + attach
                + ", body=" + body + ", mch_id=" + mch_id + ", nonce_str="
                + nonce_str + ", notify_url=" + notify_url + ", out_trade_no="
                + out_trade_no + ", total_fee=" + total_fee + ", trade_type="
                + trade_type + ", spbill_create_ip=" + spbill_create_ip
                + ", openid=" + openid + ", sign=" + sign + ", prepay_id="
                + prepay_id + ", signType=" + signType + ", timeStamp="
                + timeStamp + ", packageStr=" + packageStr + ", return_code="
                + return_code + ", return_msg=" + return_msg + ", result_code="
                + result_code + ", bank_type=" + bank_type + ", cash_fee="
                + cash_fee + ", fee_type=" + fee_type + ", is_subscribe="
                + is_subscribe + ", time_end=" + time_end + ", transaction_id="
                + transaction_id + ", ip=" + ip + ", coupon_count="
                + coupon_count + ", coupon_fee=" + coupon_fee
                + ", coupon_fee_0=" + coupon_fee_0 + ", coupon_type_0="
                + coupon_type_0 + ", coupon_id_0=" + coupon_id_0 + "]";
    }
}
WxPaySendDataPO

 

 

MD5Util:

技术分享
package com.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * md5加密算法实现
 */
public class MD5Util {
    
    private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5","6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
    
    /**
     * md5加密
     * 
     * @param text 需要加密的文本
     * @return
     */
    public static String encode(String text) {
        try {// aes rsa
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] result = md.digest(text.getBytes()); // 对文本进行加密
            // b
            // 000000..0000011111111
            StringBuilder sb = new StringBuilder();
            for (byte b : result) {
                int i = b & 0xff ; // 取字节的后八位有效值
                String s = Integer.toHexString(i);
                if (s.length() < 2) {
                    s = "0" + s;
                }
                sb.append(s);
            }
            
            // 加密的结果
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            // 找不到该算法,一般不会进入这里
            e.printStackTrace();
        }
        
        return "";
    }

     public static String MD5Encode(String origin, String charsetname) {
         String resultString = null;
         try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {

        }
        return resultString;
    }
     
     private static String byteArrayToHexString(byte b[]) {
         StringBuffer resultSb = new StringBuffer();
         for (int i = 0; i < b.length; i++)
             resultSb.append(byteToHexString(b[i]));

         return resultSb.toString();
     }

     private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
     }
}
MD5Util

 

 

WXPayUtil:

技术分享
package com.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;


import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.dom4j.DocumentException;




public class WXPayUtil {
    
    
    /**
     * 统一下单
     * 获得PrePayId
     * @param body   商品或支付单简要描述
     * @param out_trade_no 商户系统内部的订单号,32个字符内、可包含字母
     * @param total_fee  订单总金额,单位为分
     * @param IP    APP和网页支付提交用户端ip
     * @param notify_url 接收微信支付异步通知回调地址
     * @param openid 用户openId
     * @throws IOException
     * @throws DocumentException 
     */
    public static Map unifiedorder(String nonce_str,String body,String out_trade_no,String total_fee,String ip,String notify_url,String openId)throws IOException, DocumentException {
        /**
         * 设置访问路径
         */
        HttpPost httppost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder");
        //String nonce_str = getNonceStr();//随机数据
        SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
        /**
         * 组装请求参数
         * 按照ASCII排序
         */
        parameters.put("appid",WXUtil.APPID);
        parameters.put("body", body);
        parameters.put("mch_id", WXUtil.MCH_ID );
        parameters.put("nonce_str", nonce_str);
        parameters.put("out_trade_no", out_trade_no);
        parameters.put("notify_url", notify_url);
        parameters.put("spbill_create_ip", ip);
        parameters.put("total_fee",total_fee );
        parameters.put("trade_type",WXUtil.TRADE_TYPE );
        parameters.put("openid", openId);
        
        String sign = WXSignUtil.createSign(parameters, WXUtil.KEY);

        /**
         * 组装XML
         */
        StringBuilder sb = new StringBuilder("");
        sb.append("<xml>");
        setXmlKV(sb,"appid",WXUtil.APPID);
        setXmlKV(sb,"body",body);
        setXmlKV(sb,"mch_id",WXUtil.MCH_ID);
        setXmlKV(sb,"nonce_str",nonce_str);
        setXmlKV(sb,"notify_url",notify_url);
        setXmlKV(sb,"out_trade_no",out_trade_no);
        setXmlKV(sb,"spbill_create_ip",ip);
        setXmlKV(sb,"total_fee",total_fee);
        setXmlKV(sb,"trade_type",WXUtil.TRADE_TYPE);
        setXmlKV(sb,"sign",sign);
        setXmlKV(sb,"openid",openId);
        sb.append("</xml>");

        StringEntity reqEntity = new StringEntity(new String (sb.toString().getBytes("UTF-8"),"ISO8859-1"));//这个处理是为了防止传中文的时候出现签名错误
        httppost.setEntity(reqEntity);
        DefaultHttpClient httpclient = new DefaultHttpClient();
        HttpResponse response = httpclient.execute(httppost);
        String strResult = EntityUtils.toString(response.getEntity(), Charset.forName("utf-8"));

        return XMLUtil.doXMLParse(strResult);

    }


    //获得随机字符串
    public static String getNonceStr(){
         Random random = new Random();
         return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
    }

    //插入XML标签
    public static StringBuilder setXmlKV(StringBuilder sb,String Key,String value){
        sb.append("<");
        sb.append(Key);
        sb.append(">");

        sb.append(value);

        sb.append("</");
        sb.append(Key);
        sb.append(">");

        return sb;
    }

    //解析XML  获得 PrePayId
    public static String getPrePayId(String xml){
        int start = xml.indexOf("<prepay_id>");
        int end = xml.indexOf("</prepay_id>");
        if(start < 0 && end < 0){
            return null;
        }
        return xml.substring(start + "<prepay_id>".length(),end).replace("<![CDATA[","").replace("]]>","");
    }
    
    //商户订单号
    public static String getOut_trade_no(String start,String end){
        DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        String str=start+df.format(new Date())+end;
        String regex="\\w{14,32}";
        if(!str.matches(regex)){
            throw new RuntimeException("订单号格式错误");
        }
        return  str;
    }
    
    //时间戳
    public static String getTimeStamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }
    
    //随机4位数字
    public static int buildRandom(int length) {
        int num = 1;
        double random = Math.random();
        if (random < 0.1) {
            random = random + 0.1;
        }
        for (int i = 0; i < length; i++) {
            num = num * 10;
        }
        return (int) (random * num);
    }
    
    public static String inputStream2String(InputStream inStream, String encoding){
         String result = null;
         try {
         if(inStream != null){
          ByteArrayOutputStream outStream = new ByteArrayOutputStream();
          byte[] tempBytes = new byte[1024];
          int count = -1;
          while((count = inStream.read(tempBytes, 0, 1024)) != -1){
            outStream.write(tempBytes, 0, count);
          }
          tempBytes = null;
          outStream.flush();
          result = new String(outStream.toByteArray(), encoding);
         }
         } catch (Exception e) {
         result = null;
         }
         return result;
        }
    /**
     * 生成length为随机数
     * @param length
     * @return
     */
    public static String getRandomStr(int length) {
        String val = "";
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            // 输出字母还是数字
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; 
            // 字符串
            if ("char".equalsIgnoreCase(charOrNum)) {
                // 取得大写字母还是小写字母
                int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; 
                val += (char) (choice + random.nextInt(26));
            } else if ("num".equalsIgnoreCase(charOrNum)) { // 数字
                val += String.valueOf(random.nextInt(10));
            }
        }
        return val;
    }
    
    public static void main(String[] args) throws IOException, DocumentException {
//        System.out.println(getOut_trade_no("",""));
//        //openID
        String openId="";
//       
//        //统一下单
        String nonce_str=getNonceStr();
        String str="|";
        String out_trade_no = WXPayUtil.getOut_trade_no("testpay",WXPayUtil.getRandomStr(6));
        Map strResult = WXPayUtil.unifiedorder(nonce_str,"test", out_trade_no, "1", "127.0.0.1", "http://www.baidu.com",openId);
        System.out.println(strResult);
//        //Map map=XMLUtil.doXMLParse(strResult);
//        SortedMap<Object,Object> resultMap=(TreeMap)XMLUtil.doXMLParse(strResult);
//        System.out.println(resultMap);
//        String regex="[0-9a-zA-Z\\_\\-\\*\\|@]{1,32}";
//        System.out.println(nonce_str.length());
        System.out.println(out_trade_no.length());
    }
    
}
WXPayUtil

 

WXSignUtil:

技术分享
package com.util;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

import com.job37.jser.core.util.MD5Util;

public class WXSignUtil {
          
        /**
          * 创建签名
          * @param parameters
          * @param key
          * @return
          */
            @SuppressWarnings("rawtypes")
            public static String createSign(SortedMap<Object,Object> parameters,String key){  
                 StringBuffer sb = new StringBuffer();
                 Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
                 Iterator it = es.iterator();
                 while(it.hasNext()) {
                     Map.Entry entry = (Map.Entry)it.next();
                     String k = (String)entry.getKey();
                     Object v = entry.getValue();
                     if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
                         sb.append(k + "=" + v + "&");
                     }
                 }
                 sb.append("key=" + key);
                 String sign = MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
                 return sign;
            }
}
WXSignUtil

 

WXUtil:

技术分享
package com.util;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.json.JSONObject;

import com.alibaba.dubbo.common.json.JSON;
import com.job37.jser.core.constants.GlobalConstants;
import com.job37.jser.core.exceptions.CustomGlobalException;
import com.job37.jser.core.util.RedisUtil;

import redis.clients.jedis.Jedis;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

public class WXUtil {
    //服务号ID
    public static final String APPID="";
    //服务号密码
    public static final String APPSECRET="";
    //上齐猎微信支付商户号
    public static final String MCH_ID="";
    public static final String TRADE_TYPE="JSAPI";
    //api密钥
    public static final String KEY="";
    //检查微信登录的URL
    public static final String  WXLOGIN_URL="";
    //微信支付,送贺礼成功,异步回调的URL
    public static final String GIFTAGENT_NOTIFY_URL = "";
    
    
    
    /*******************************************************************************/
    
    
    /**
     * 获取请求用户信息的access_token
     *
     * @param code
     * @return
     */
    public static Map<String, Object> getUserInfoAccessToken(String code) {
        Map<String, Object> data = new HashMap();
        try {
            String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
                                       APPID, APPSECRET, code);
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String tokens = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(tokens,Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }
    
    public static Map<String, Object> getAccessToken(String code) {
        Map<String, Object> data = new HashMap();
        try {
            String appid="";//商户的APPID
            String appsecret="";//商户的APPSECRET
                    
            String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
                    appid, appsecret, code);
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String tokens = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(tokens,Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }
    
    /**
     * 获取用户信息
     *
     * @param accessToken
     * @param openId
     * @return
     */
    public static Map<String, Object> getUserInfo(String accessToken, String openId) {
        Map<String, Object> data = new HashMap();
        String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
        try {
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String responseStr = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(responseStr,Map.class);
        } catch (Exception ex) {
        }
        return data;
    }
    
    /**
     * 生成用于获取access_token的Code的Url
     *
     * @param redirectUrl
     * @return
     */
    public static String getRequestCodeUrl(String redirectUrl) {
        return String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect",
                             APPID, redirectUrl, "snsapi_userinfo", "xxxx_state");
    }
    
   
    
    public static Map<String, Object> testGetUserInfo(String accessToken, String openId) {
        Map<String, Object> data = new HashMap();
        String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
        String url2="https://api.weixin.qq.com/cgi-bin/user/info?access_token="+ accessToken + "&openid=" + openId + "&lang=zh_CN";;
        try {
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String responseStr = EntityUtils.toString(httpEntity, "utf-8");
            data=JSON.parse(responseStr,Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }
    
    public static String getTicket(String accessToken){
        String ticket = null;  
        String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+ accessToken +"&type=jsapi";//这个url链接和参数不能变  
        try {  
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String responseStr = EntityUtils.toString(httpEntity, "utf-8");
//            System.out.println(responseStr);
            Map  data=JSON.parse(responseStr,Map.class);
            ticket=data.get("ticket")+"";
        } catch (Exception e) {  
                e.printStackTrace();  
        }  
        return ticket;  
    }
    
    public static String getAccessToken(){
        String token="";
        RedisUtil ut=new RedisUtil();
        Jedis jedis=ut.getJedis();
        try {
            jedis.select(6);
            token=jedis.get("ACCESS_TOKEN");
            if(token==null||token.trim().equals("")||"null".equals(token)){
                token=refreshAccessToken();
                jedis.set("ACCESS_TOKEN", token);
                jedis.expire("ACCESS_TOKEN", 7000);
            }
        } catch (Exception e) {
            throw e;
        }finally{
            ut.returnResource(jedis);
        }
        return token;
    }
    
    public static String refreshAccessToken() {  
        String access_token = "";  
        String grant_type = "client_credential";//获取access_token填写client_credential   
        //这个url链接地址和参数皆不能变  
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type="+grant_type+"&appid="+APPID+"&secret="+APPSECRET;  
           
        try {
            
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            String responseStr = EntityUtils.toString(httpEntity, "utf-8");
            //System.out.println(responseStr);
            Map  data=JSON.parse(responseStr,Map.class);
            access_token=data.get("access_token")+"";
        } catch (Exception e) {  
                e.printStackTrace();  
        }  
        return access_token;  
    }  
    
    /**
     * 获取微信配置的签名
     * @param jsapi_ticket
     * @param url
     * @param timeStamp
     * @param nonce_str
     * @return
     */
    public static String getSHA1Sign(String jsapi_ticket, String url,String timeStamp,String nonce_str){
         String decript = "jsapi_ticket=" + jsapi_ticket +  
                 "&noncestr=" + nonce_str +  
                 "&timestamp=" + timeStamp +  
                 "&url=" + url;  
         return SHA1(decript);
         
    }
    
    /**
     * 获取微信配置的签名
     * @param url
     * @param timeStamp
     * @param nonce_str
     * @return
     */
    public static String getSHA1Sign( String url,String timeStamp,String nonce_str){
        String token=getAccessToken();
        String ticket=getTicket(token);
         String decript = "jsapi_ticket=" + ticket +  
                 "&noncestr=" + nonce_str +  
                 "&timestamp=" + timeStamp +  
                 "&url=" + url;  
         return SHA1(decript);
         
    }
    
    public static String SHA1(String decript) {  
        try {  
            MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");  
            digest.update(decript.getBytes());  
            byte messageDigest[] = digest.digest();  
            // Create Hex String  
            StringBuffer hexString = new StringBuffer();  
            // 字节数组转换为 十六进制 数  
                for (int i = 0; i < messageDigest.length; i++) {  
                    String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);  
                    if (shaHex.length() < 2) {  
                        hexString.append(0);  
                    }  
                    hexString.append(shaHex);  
                }  
                return hexString.toString();  
       
            } catch (NoSuchAlgorithmException e) {  
                e.printStackTrace();  
                return "";  
            }  
    }
    //获取微信授权登录的URL
    public static String getLoginRedirectUrl(String redirectUrl){
//        String  redirect_uri=TEST_WXLOGIN_URL+redirectUrl;
        String  redirect_uri=WXLOGIN_URL+redirectUrl;
        try {
            redirect_uri=java.net.URLEncoder.encode(redirect_uri,"utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new CustomGlobalException("参数错误");
        }
        return "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+APPID+"&redirect_uri="+redirect_uri+"&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
    }
    
    
    

    
    /**
     * 获取完整的URL
     * @param request
     * @param midfix 链接的中间字符串
     * @return
     */
    public static String getWholeURL(HttpServletRequest request,String midfix){
        return GlobalConstants.SHARE_URL+midfix+"?" + request.getQueryString();
    }
    

    
}
WXUtil

 

XMLUtil:

技术分享
package com.util;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;




public class XMLUtil { 
/** 
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 
* @param strxml 
* @return 
* @throws JDOMException 
* @throws IOException 
 * @throws DocumentException 
*/ 
public static Map doXMLParse(String strxml) throws  IOException, DocumentException { 
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

    if(null == strxml || "".equals(strxml)) {
        return null;
    }

    Map m = new TreeMap();

    InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
    SAXReader builder = new SAXReader();
    Document doc =  builder.read(in);
    Element root = doc.getRootElement();
    Iterator it = root.elementIterator();
    while(it.hasNext()) {
        Element e = (Element) it.next();
        String k = e.getName();
        String v = e.getText();
//        List children = e.getChildren();
//        if(children.isEmpty()) {
//            v = e.getTextNormalize();
//        } else {
//            v = XMLUtil.getChildrenText(children);
//        }

        m.put(k, v);
    }

    //关闭流
    in.close();

    return m;
}

/**
 * 获取子结点的xml
 * @param children
 * @return String
 * @throws DocumentException 
 * @throws IOException 
 */
//public static String getChildrenText(List children) {
//    StringBuffer sb = new StringBuffer();
//    if(!children.isEmpty()) {
//        Iterator it = children.iterator();
//        while(it.hasNext()) {
//            Element e = (Element) it.next();
//            String name = e.getName();
//            String value = e.getTextNormalize();
//            List list = e.getChildren();
//            sb.append("<" + name + ">");
//            if(!list.isEmpty()) {
//                sb.append(XMLUtil.getChildrenText(list));
//            }
//            sb.append(value);
//            sb.append("</" + name + ">");
//        }
//    }
//
//    return sb.toString();
//}
    public static void main(String[] args) throws IOException, DocumentException {
        String xml="<xml><id>555</id>"+
                    "<name>hello</name></xml>";
        Map<String,Object>m=XMLUtil.doXMLParse(xml);
        System.out.println(m);
    }
} 
XMLUtil

 

 

五、H5页面调起微信支付的js

1)要调用微信的支付js,必须在页面引入<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>,并且配置微信支付

微信js配置(wx.js)如下:

    var paySign ;
   var appId;
    var timeStamp ;
    var nonceStr ;
    var packageStr ;
    var signType ;
    var signature;
    
    function wxPay(){
        //console.log(signature);
    //通过config接口注入权限验证配置 
            wx.config({
                debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                appId: appId, // 必填,公众号的唯一标识
                timestamp:timeStamp , // 必填,生成签名的时间戳
                nonceStr: nonceStr, // 必填,生成签名的随机串
                signature: signature,// 必填,签名,见附录1
                jsApiList: [‘chooseWXPay‘] // 必填,需要使用的JS接口列表,这里只写支付的
            });
    //通过ready接口处理成功验证
            wx.ready(function(){
                //wx.hideOptionMenu();//隐藏右边的一些菜单
                wx.chooseWXPay({
                    timestamp: timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
                    nonceStr: nonceStr, // 支付签名随机串,不长于 32 位
                    package: packageStr, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
                    signType: signType, // 签名方式,默认为‘SHA1‘,使用新版支付需传入‘MD5‘
                    paySign: paySign, // 支付签名
                    success: function (res) {
                        // 支付成功后的回调函数
                        layer.closeAll();
                        alert("支付成功");
                    },
                    //如果你按照正常的jQuery逻辑,下面如果发送错误,一定是error,那你就太天真了,当然,jssdk文档中也有提到
                    fail: function(res) {
                        //接口调用失败时执行的回调函数。
                        layer.closeAll();
                        alert("支付失败,请稍后再试");
                    },
                    complete: function(res) {
                        //接口调用完成时执行的回调函数,无论成功或失败都会执行。
                        //alert("complete");
                    },
                    cancel: function(res) {
                        //用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。
                        //alert("cancel");
                    },
                    trigger: function(res) {
                        //监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。
                        //alert("trigger");
                    }
                });
                
            });
    }
    
   function testPay(){
     //调用微信支付
        //callpay();
        var url=‘../../../guest/WXResource/getPrepayid‘;
        var giftMoney=$("#giftMoney").val();
        var param={giftMoney:giftMoney,identityId:identityId};
        $.post(url,param,function(res){
            var code=res.code;
            var data=res.data;
            if(code==1){
                 paySign=data.sign ;
                 appId=data.appid;
                 timeStamp=data.timeStamp ;
                 nonceStr=data.nonce_str ;
                 packageStr=data.packageStr ;
                 signType=data.signType ;
                 signature=res.signature;
                 wxPay();
                 //callpay();
            }else{
                layer.open({
                    content:res.msg
                });
            }
        });
   }
    
    

配置好后,在页面写个点击事件,调用testPay函数,就会初始化微信支付的配置,随后调起js支付。

六、异步回调

支付成功后,要在异步回调的URL(这个在统一下单中就定义了notify_url)中处理相应的业务

示例回调代码:

/**
     * 支付回调接口
    * @param request
    * @return
     */
    @Path("payCallback")
    @POST
    public void callBack(){
//        System.out.println("gift_payCallBack...");
        response.setContentType("text/xml;charset=UTF-8");
        try {
            InputStream is = request.getInputStream();
            String resultStr = IOUtils.toString(is, "UTF-8");
                if("".equals(resultStr)){
                    System.out.println("fail:result is null");
                    response.getWriter().write("<xm><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[参数错误!]]></return_msg></xml>");
                    return ;
                }
            //解析xml
                SortedMap<Object,Object> resultMap=(TreeMap)XMLUtil.doXMLParse(resultStr);
//                System.out.println(resultMap);
                String sign = resultMap.get("sign")+"";
                String sign2 = WXSignUtil.createSign(resultMap, WXUtil.KEY);
                String result_code=resultMap.get("result_code")+"";
                String return_code=resultMap.get("return_code")+"";
                if(sign.equals(sign2)){//校验签名,两者需要一致,防止别人绕过支付操作,不付钱直接调用你的业务,不然,哈哈,你老板会很开心的 233333.。。。
                     if(return_code.equals("SUCCESS") && result_code.equals("SUCCESS")){
                        //业务逻辑(先判断数据库中订单号是否存在,并且订单状态为未支付状态)
                         try {
                             //resultMap.put("openid", request.getSession().getAttribute("openid"));
                            
                            
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                         //request.setAttribute("out_trade_no", out_trade_no);
                         //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
                         response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
                     }else{
                         response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[交易失败]]></return_msg></xml>");
                     }
                }else{
                     //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
                    response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名校验失败]]></return_msg></xml>");
                }
                response.getWriter().flush();
                response.getWriter().close();
                return;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 

 

 

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!