微信app支付java后台流程、原理分析及nei网穿透

一.流程步骤

  本实例是基于springmvc框架编写

  1.执行流程
           当手机端app(就是你公司开发的app)在支付页面时,调起服务端(后台第1个创建订单接口)接口,后台把需要调起微信支付的参数返回给手机端,手机端拿到
         这些参数后,拉起微信支付环境完成支付,完成支付后会调异步通知(第2个接口),此时需要给微信返回成功或者失败信息,成功后,由app端调用同步通知(第3个接口)
         返回支付成功页面,完成整个支付流程。
        
      2.需要说明的事项

        因为微信支付都是用自己工具类生成加密、解析xml、验签等方法,需要写的类比较多,因此大家在参考此文档时,直接复制就行了

二.配置文件及通用类

  1.配置文件类

  ConstantUtil(各应应用id的配置,把自己应用对应的找到即可

 1 package com.qtkj.app.weixinpay.util;
 2 
 3 public class ConstantUtil {
 4    
 5     // 微信开发平台应用ID*             
 6     public static final String APP_ID="";
 7     
 8     // 应用对应的凭证  appsecret                  
 9     public static final String APP_SECRET="";
10     
11     // 应用对应的密钥 appkey                 
12     public static final String APP_KEY="";
13    
14     //微信支付商户号 
15     public static final String MCH_ID="";
16     
17     //商户id 
18     public static final String PARTNER_ID="";
19     
20     //商品描述
21     public static final String BODY="游戏币-账户充值";
22     
23      // 获取预支付id的接口访问路径 
24     public static String GATEURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
25     
26     // 微信服务器回调通知url
27     public static String NOTIFY_URL="";
28 }

2.工具类(一共8个,直接复制使用即可) 

 TenpayUtil

  1 package com.qtkj.app.weixinpay.util;
  2 
  3 import java.text.SimpleDateFormat;
  4 import java.util.Date;
  5 
  6 import javax.servlet.http.HttpServletRequest;
  7 import javax.servlet.http.HttpServletResponse;
  8 
  9 public class TenpayUtil {
 10     
 11     /**
 12      * 把对象转换成字符串
 13      * @param obj
 14      * @return String 转换成字符串,若对象为null,则返回空字符串.
 15      */
 16     public static String toString(Object obj) {
 17         if(obj == null)
 18             return "";
 19         
 20         return obj.toString();
 21     }
 22     
 23     /**
 24      * 把对象转换为int数值.
 25      * 
 26      * @param obj
 27      *            包含数字的对象.
 28      * @return int 转换后的数值,对不能转换的对象返回0。
 29      */
 30     public static int toInt(Object obj) {
 31         int a = 0;
 32         try {
 33             if (obj != null)
 34                 a = Integer.parseInt(obj.toString());
 35         } catch (Exception e) {
 36 
 37         }
 38         return a;
 39     }
 40     
 41     /**
 42      * 获取当前时间 yyyyMMddHHmmss
 43      * @return String
 44      */ 
 45     public static String getCurrTime() {
 46         Date now = new Date();
 47         SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
 48         String s = outFormat.format(now);
 49         return s;
 50     }
 51     
 52     /**
 53      * 获取当前日期 yyyyMMdd
 54      * @param date
 55      * @return String
 56      */
 57     public static String formatDate(Date date) {
 58         SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
 59         String strDate = formatter.format(date);
 60         return strDate;
 61     }
 62     
 63     /**
 64      * 取出一个指定长度大小的随机正整数.
 65      * 
 66      * @param length
 67      *            int 设定所取出随机数的长度。length小于11
 68      * @return int 返回生成的随机数。
 69      */
 70     public static int buildRandom(int length) {
 71         int num = 1;
 72         double random = Math.random();
 73         if (random < 0.1) {
 74             random = random + 0.1;
 75         }
 76         for (int i = 0; i < length; i++) {
 77             num = num * 10;
 78         }
 79         return (int) ((random * num));
 80     }
 81     
 82     /**
 83      * 获取编码字符集
 84      * @param request
 85      * @param response
 86      * @return String
 87      */
 88     public static String getCharacterEncoding(HttpServletRequest request,
 89             HttpServletResponse response) {
 90         
 91         if(null == request || null == response) {
 92             return "gbk";
 93         }
 94         
 95         String enc = request.getCharacterEncoding();
 96         if(null == enc || "".equals(enc)) {
 97             enc = response.getCharacterEncoding();
 98         }
 99         
100         if(null == enc || "".equals(enc)) {
101             enc = "gbk";
102         }
103         
104         return enc;
105     }
106     
107     /**
108      * 获取unix时间,从1970-01-01 00:00:00开始的秒数
109      * @param date
110      * @return long
111      */
112     public static long getUnixTime(Date date) {
113         if( null == date ) {
114             return 0;
115         }
116         
117         return date.getTime()/1000;
118     }
119         
120     /**
121      * 时间转换成字符串
122      * @param date 时间
123      * @param formatType 格式化类型
124      * @return String
125      */
126     public static String date2String(Date date, String formatType) {
127         SimpleDateFormat sdf = new SimpleDateFormat(formatType);
128         return sdf.format(date);
129     }
130 }

  PrepayIdRequestHandler

 1 package com.qtkj.app.weixinpay.handler;
 2 
 3 import java.util.Iterator;
 4 import java.util.Map;
 5 import java.util.Set;
 6 
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 import com.qtkj.app.weixinpay.util.ConstantUtil;
11 import com.qtkj.app.weixinpay.util.MD5Util;
12 import com.qtkj.app.weixinpay.util.XMLUtil;
13 
14 public class PrepayIdRequestHandler extends RequestHandler {
15 
16     public PrepayIdRequestHandler(HttpServletRequest request,
17             HttpServletResponse response) {
18         super(request, response);
19     }
20 
21     public String createMD5Sign() {
22         StringBuffer sb = new StringBuffer();
23         Set es = super.getAllParameters().entrySet();
24         Iterator it = es.iterator();
25         while (it.hasNext()) {
26             Map.Entry entry = (Map.Entry) it.next();
27             String k = (String) entry.getKey();
28             String v = (String) entry.getValue();
29             sb.append(k + "=" + v + "&");
30         }
31         String params=sb.append("key="+ConstantUtil.APP_KEY).substring(0);
32
33         
34         String sign = MD5Util.MD5Encode(params, "utf8");
35         return sign.toUpperCase();
36     }
37 
38     // 提交预支付
39     public String sendPrepay() throws Exception {
40         String prepayid = "";
41         Set es=super.getAllParameters().entrySet();
42         Iterator it=es.iterator();
43         StringBuffer sb = new StringBuffer("<xml>");
44         while(it.hasNext()){
45             Map.Entry entry = (Map.Entry) it.next();
46             String k = (String) entry.getKey();
47             String v = (String) entry.getValue();
48             sb.append("<"+k+">"+v+"</"+k+">");
49         }
50         sb.append("</xml>");
51         String params=sb.substring(0);
52         System.out.println("请求参数:"+params);
53         String requestUrl = super.getGateUrl();
54         System.out.println("请求url:"+requestUrl);
55         TenpayHttpClient httpClient = new TenpayHttpClient();
56         httpClient.setReqContent(requestUrl);
57         String resContent = "";
58         if (httpClient.callHttpPost(requestUrl, params)) {
59             resContent = httpClient.getResContent();
60             System.out.println("获取prepayid的返回值:"+resContent);
61             Map<String,String> map=XMLUtil.doXMLParse(resContent);
62             if(map.containsKey("prepay_id"))
63                 prepayid=map.get("prepay_id");
64         }
65         return prepayid;
66     }
67 }

  RequestHandler

  1 package com.qtkj.app.weixinpay.handler;
  2 
  3 import java.io.IOException;
  4 import java.io.UnsupportedEncodingException;
  5 import java.net.URLEncoder;
  6 import java.util.Iterator;
  7 import java.util.Map;
  8 import java.util.Set;
  9 import java.util.SortedMap;
 10 import java.util.TreeMap;
 11 
 12 import javax.servlet.http.HttpServletRequest;
 13 import javax.servlet.http.HttpServletResponse;
 14 
 15 import com.qtkj.app.weixinpay.util.MD5Util;
 16 import com.qtkj.app.weixinpay.util.TenpayUtil;
 17 
 18 /**
 19  * 请求处理类
 20  * 请求处理类继承此类,重写createSign方法即可。
 21  *
 22  */
 23 public class RequestHandler {
 24     
 25     /** 网关url地址 */
 26     private String gateUrl;
 27     
 28     /** 密钥 */
 29     private String key;
 30     
 31     /** 请求的参数 */
 32     private SortedMap parameters;
 33     
 34     protected HttpServletRequest request;
 35     
 36     protected HttpServletResponse response;
 37     
 38     /**
 39      * 构造函数
 40      * @param request
 41      * @param response
 42      */
 43     public RequestHandler(HttpServletRequest request, HttpServletResponse response) {
 44         this.request = request;
 45         this.response = response;
 46         
 47         this.gateUrl = "https://gw.tenpay.com/gateway/pay.htm";
 48         this.key = "";
 49         this.parameters = new TreeMap();
 50     }
 51     
 52     /**
 53     *初始化函数。
 54     */
 55     public void init() {
 56         //nothing to do
 57     }
 58 
 59     /**
 60     *获取入口地址,不包含参数值
 61     */
 62     public String getGateUrl() {
 63         return gateUrl;
 64     }
 65 
 66     /**
 67     *设置入口地址,不包含参数值
 68     */
 69     public void setGateUrl(String gateUrl) {
 70         this.gateUrl = gateUrl;
 71     }
 72 
 73     /**
 74     *获取密钥
 75     */
 76     public String getKey() {
 77         return key;
 78     }
 79 
 80     /**
 81     *设置密钥
 82     */
 83     public void setKey(String key) {
 84         this.key = key;
 85     }
 86     
 87     /**
 88      * 获取参数值
 89      * @param parameter 参数名称
 90      * @return String 
 91      */
 92     public String getParameter(String parameter) {
 93         String s = (String)this.parameters.get(parameter); 
 94         return (null == s) ? "" : s;
 95     }
 96     
 97     /**
 98      * 设置参数值
 99      * @param parameter 参数名称
100      * @param parameterValue 参数值
101      */
102     public void setParameter(String parameter, Object parameterValue) {
103         String v = "";
104         if(null != parameterValue) {
105             if(parameterValue instanceof String)
106             v = ((String) parameterValue).trim();
107         }
108         this.parameters.put(parameter, v);
109     }
110     
111     /**
112      * 返回所有的参数
113      * @return SortedMap
114      */
115     public SortedMap getAllParameters() {        
116         return this.parameters;
117     }
118     
119     /**
120      * 获取带参数的请求URL
121      * @return String
122      * @throws UnsupportedEncodingException 
123      */
124     public String getRequestURL() throws UnsupportedEncodingException {
125         
126         this.createSign();
127         
128         StringBuffer sb = new StringBuffer();
129         String enc = TenpayUtil.getCharacterEncoding(this.request, this.response);
130         Set es = this.parameters.entrySet();
131         Iterator it = es.iterator();
132         while(it.hasNext()) {
133             Map.Entry entry = (Map.Entry)it.next();
134             String k = (String)entry.getKey();
135             String v = (String)entry.getValue();
136             
137             if(!"spbill_create_ip".equals(k)) {
138                 sb.append(k + "=" + URLEncoder.encode(v, enc) + "&");
139             } else {
140                 sb.append(k + "=" + v.replace("\\.", "%2E") + "&");
141             }
142         }
143         
144         //去掉最后一个&
145         String reqPars = sb.substring(0, sb.lastIndexOf("&"));
146         
147         return this.getGateUrl() + "?" + reqPars;
148         
149     }
150     
151     public void doSend() throws UnsupportedEncodingException, IOException {
152         this.response.sendRedirect(this.getRequestURL());
153     }
154     
155     /**
156      * 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
157      */
158     protected void createSign() {
159         StringBuffer sb = new StringBuffer();
160         Set es = this.parameters.entrySet();
161         Iterator it = es.iterator();
162         while(it.hasNext()) {
163             Map.Entry entry = (Map.Entry)it.next();
164             String k = (String)entry.getKey();
165             String v = (String)entry.getValue();
166             if(null != v && !"".equals(v) 
167                     && !"sign".equals(k) && !"key".equals(k)) {
168                 sb.append(k + "=" + v + "&");
169             }
170         }
171         sb.append("key=" + this.getKey());
172         String enc = TenpayUtil.getCharacterEncoding(this.request, this.response);
173         String sign = MD5Util.MD5Encode(sb.toString(), enc).toUpperCase();
174         
175         this.setParameter("sign", sign);
176         
177     }
178     
179     protected HttpServletRequest getHttpServletRequest() {
180         return this.request;
181     }
182     
183     protected HttpServletResponse getHttpServletResponse() {
184         return this.response;
185     }
186 }

  MD5Util

 1 /**
 2  * 
 3  */
 4 package com.qtkj.app.weixinpay.util;
 5 
 6 import java.security.MessageDigest;
 7 
 8 /**
 9 * @author Zhao
10 * @version 创建时间:2017年10月22日 下午3:24:07 
11 * 
12 */
13 /**
14 *<p>Title:MD5Util </p>
15 *<p>Description:</p>
16 *<p>Company:</p> 
17 *@author ZHAO
18 *@date 2017年10月22日
19 */
20 public class MD5Util {
21      /**
22      * MD5加密
23      * @param b
24      * @return
25      */
26     private static String byteArrayToHexString(byte b[]) {
27         StringBuffer resultSb = new StringBuffer();
28         for (int i = 0; i < b.length; i++)
29             resultSb.append(byteToHexString(b[i]));
30 
31         return resultSb.toString();
32     }
33 
34     private static String byteToHexString(byte b) {
35         int n = b;
36         if (n < 0)
37             n += 256;
38         int d1 = n / 16;
39         int d2 = n % 16;
40         return hexDigits[d1] + hexDigits[d2];
41     }
42 
43     public static String MD5Encode(String origin, String charsetname) {
44         String resultString = null;
45         try {
46             resultString = new String(origin);
47             MessageDigest md = MessageDigest.getInstance("MD5");//MD5加密
48             if (charsetname == null || "".equals(charsetname)){
49                 resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
50             }else{
51                 resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
52             }
53         } catch (Exception exception) {
54         }
55         return resultString;
56     }
57 
58     private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
59             "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
60 }

TenpayHttpClient

  1 package com.qtkj.app.weixinpay.handler;
  2 
  3 import java.io.BufferedOutputStream;
  4 import java.io.IOException;
  5 import java.io.InputStream;
  6 import java.net.HttpURLConnection;
  7 
  8 import javax.net.ssl.HttpsURLConnection;
  9 import javax.net.ssl.SSLContext;
 10 import javax.net.ssl.SSLSocketFactory;
 11 
 12 
 13 import com.qtkj.app.weixinpay.util.HttpClientUtil;
 14 
 15 public class TenpayHttpClient {
 16 
 17     
 18 
 19     
 20     /** 请求内容,无论post和get,都用get方式提供 */
 21     private String reqContent;
 22     
 23     /** 应答内容 */
 24     private String resContent;
 25     
 26     /** 请求方法 */
 27     private String method;
 28     
 29     /** 错误信息 */
 30     private String errInfo;
 31     
 32     /** 超时时间,以秒为单位 */
 33     private int timeOut;
 34     
 35     /** http应答编码 */
 36     private int responseCode;
 37     
 38     /** 字符编码 */
 39     private String charset;
 40     
 41     private InputStream inputStream;
 42     
 43     public TenpayHttpClient() {
 44         this.reqContent = "";
 45         this.resContent = "";
 46         this.method = "POST";
 47         this.errInfo = "";
 48         this.timeOut = 30;//30秒
 49         
 50         this.responseCode = 0;
 51         this.charset = "utf8";
 52         
 53         this.inputStream = null;
 54     }
 55     
 56     /**
 57      * 设置请求内容
 58      * @param reqContent 表求内容
 59      */
 60     public void setReqContent(String reqContent) {
 61         this.reqContent = reqContent;
 62     }
 63     
 64     /**
 65      * 获取结果内容
 66      * @return String
 67      * @throws IOException 
 68      */
 69     public String getResContent() {
 70         try {
 71             this.doResponse();
 72         } catch (IOException e) {
 73             this.errInfo = e.getMessage();
 74             //return "";
 75         }
 76         
 77         return this.resContent;
 78     }
 79     
 80     /**
 81      * 设置请求方法post或者get
 82      * @param method 请求方法post/get
 83      */
 84     public void setMethod(String method) {
 85         this.method = method;
 86     }
 87     
 88     /**
 89      * 获取错误信息
 90      * @return String
 91      */
 92     public String getErrInfo() {
 93         return this.errInfo;
 94     }
 95     
 96     /**
 97      * 设置超时时间,以秒为单位
 98      * @param timeOut 超时时间,以秒为单位
 99      */
100     public void setTimeOut(int timeOut) {
101         this.timeOut = timeOut;
102     }
103     
104     /**
105      * 获取http状态码
106      * @return int
107      */
108     public int getResponseCode() {
109         return this.responseCode;
110     }
111     
112     protected void callHttp() throws IOException {
113         
114         if("POST".equals(this.method.toUpperCase())) {
115             String url = HttpClientUtil.getURL(this.reqContent);
116             String queryString = HttpClientUtil.getQueryString(this.reqContent);
117             byte[] postData = queryString.getBytes(this.charset);
118             this.httpPostMethod(url, postData);
119             
120             return ;
121         }
122         
123         this.httpGetMethod(this.reqContent);
124         
125     } 
126     
127     public boolean callHttpPost(String url, String postdata) {
128         boolean flag = false;
129         byte[] postData;
130         try {
131             postData = postdata.getBytes(this.charset);
132             this.httpPostMethod(url, postData);
133             flag = true;
134         } catch (IOException e1) {
135             e1.printStackTrace();
136         }
137         return flag;
138     }
139     
140     /**
141      * 以http post方式通信
142      * @param url
143      * @param postData
144      * @throws IOException
145      */
146     protected void httpPostMethod(String url, byte[] postData)
147             throws IOException {
148 
149         HttpURLConnection conn = HttpClientUtil.getHttpURLConnection(url);
150 
151         this.doPost(conn, postData);
152     }
153     
154     /**
155      * 以http get方式通信
156      * 
157      * @param url
158      * @throws IOException
159      */
160     protected void httpGetMethod(String url) throws IOException {
161         
162         HttpURLConnection httpConnection =
163             HttpClientUtil.getHttpURLConnection(url);
164         
165         this.setHttpRequest(httpConnection);
166         
167         httpConnection.setRequestMethod("GET");
168         
169         this.responseCode = httpConnection.getResponseCode();
170         
171         this.inputStream = httpConnection.getInputStream();
172         
173     }
174     
175     /**
176      * 以https get方式通信
177      * @param url
178      * @param sslContext
179      * @throws IOException
180      */
181     protected void httpsGetMethod(String url, SSLContext sslContext)
182             throws IOException {
183 
184         SSLSocketFactory sf = sslContext.getSocketFactory();
185 
186         HttpsURLConnection conn = HttpClientUtil.getHttpsURLConnection(url);
187 
188         conn.setSSLSocketFactory(sf);
189 
190         this.doGet(conn);
191 
192     }
193     
194     protected void httpsPostMethod(String url, byte[] postData,
195             SSLContext sslContext) throws IOException {
196 
197         SSLSocketFactory sf = sslContext.getSocketFactory();
198 
199         HttpsURLConnection conn = HttpClientUtil.getHttpsURLConnection(url);
200 
201         conn.setSSLSocketFactory(sf);
202 
203         this.doPost(conn, postData);
204 
205     }
206     
207     /**
208      * 设置http请求默认属性
209      * @param httpConnection
210      */
211     protected void setHttpRequest(HttpURLConnection httpConnection) {
212         
213         //设置连接超时时间
214         httpConnection.setConnectTimeout(this.timeOut * 1000);
215         
216         
217         //不使用缓存
218         httpConnection.setUseCaches(false);
219         
220         //允许输入输出
221         httpConnection.setDoInput(true);
222         httpConnection.setDoOutput(true);
223         
224     }
225     
226     /**
227      * 处理应答
228      * @throws IOException
229      */
230     protected void doResponse() throws IOException {
231         
232         if(null == this.inputStream) {
233             return;
234         }
235 
236         //获取应答内容
237         this.resContent=HttpClientUtil.InputStreamTOString(this.inputStream,this.charset); 
238 
239         //关闭输入流
240         this.inputStream.close();
241         
242     }
243     
244     /**
245      * post方式处理
246      * @param conn
247      * @param postData
248      * @throws IOException
249      */
250     protected void doPost(HttpURLConnection conn, byte[] postData)
251             throws IOException {
252 
253         // 以post方式通信
254         conn.setRequestMethod("POST");
255 
256         // 设置请求默认属性
257         this.setHttpRequest(conn);
258 
259         // Content-Type
260         conn.setRequestProperty("Content-Type",
261                 "application/x-www-form-urlencoded");
262 
263         BufferedOutputStream out = new BufferedOutputStream(conn
264                 .getOutputStream());
265 
266         final int len = 1024; // 1KB
267         HttpClientUtil.doOutput(out, postData, len);
268 
269         // 关闭流
270         out.close();
271 
272         // 获取响应返回状态码
273         this.responseCode = conn.getResponseCode();
274 
275         // 获取应答输入流
276         this.inputStream = conn.getInputStream();
277 
278     }
279     
280     /**
281      * get方式处理
282      * @param conn
283      * @throws IOException
284      */
285     protected void doGet(HttpURLConnection conn) throws IOException {
286         
287         //以GET方式通信
288         conn.setRequestMethod("GET");
289         
290         //设置请求默认属性
291         this.setHttpRequest(conn);
292         
293         //获取响应返回状态码
294         this.responseCode = conn.getResponseCode();
295         
296         //获取应答输入流
297         this.inputStream = conn.getInputStream();
298     }
299 
300     
301 }
XMLUtil
  1 package com.qtkj.app.weixinpay.util;
  2 /**
  3 *<p>Title:XMLUtil </p>
  4 *<p>Description:</p>
  5 *<p>Company:</p> 
  6 *@author ZHAO
  7 *@date 2017年10月22日
  8 */
  9 import java.io.IOException;
 10 import java.io.InputStream;
 11 import java.util.HashMap;
 12 import java.util.Iterator;
 13 import java.util.List;
 14 import java.util.Map;
 15 import java.util.Map.Entry;
 16 import java.util.Set;
 17 
 18 import org.jdom2.Document;
 19 import org.jdom2.Element;
 20 import org.jdom2.JDOMException;
 21 import org.jdom2.input.SAXBuilder;
 22 import java.io.ByteArrayInputStream;
 23 public class XMLUtil {
 24     /**
 25      * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
 26      * @param strxml
 27      * @return
 28      * @throws JDOMException
 29      * @throws IOException
 30      */
 31     public static Map doXMLParse(String strxml) throws JDOMException, IOException {
 32         strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
 33         if(null == strxml || "".equals(strxml)) {
 34             return null;
 35         }
 36          
 37         Map m = new HashMap();
 38          
 39         InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
 40         SAXBuilder builder = new SAXBuilder();
 41         Document doc = builder.build(in);
 42         Element root = doc.getRootElement();
 43         List list = root.getChildren();
 44         Iterator it = list.iterator();
 45         while(it.hasNext()) {
 46             Element e = (Element) it.next();
 47             String k = e.getName();
 48             String v = "";
 49             List children = e.getChildren();
 50             if(children.isEmpty()) {
 51                 v = e.getTextNormalize();
 52             } else {
 53                 v = XMLUtil.getChildrenText(children);
 54             }
 55              
 56             m.put(k, v);
 57         }
 58          
 59         //关闭流
 60         in.close();
 61          
 62         return m;
 63     }
 64      
 65     /**
 66      * 获取子结点的xml
 67      * @param children
 68      * @return String
 69      */
 70     public static String getChildrenText(List children) {
 71         StringBuffer sb = new StringBuffer();
 72         if(!children.isEmpty()) {
 73             Iterator it = children.iterator();
 74             while(it.hasNext()) {
 75                 Element e = (Element) it.next();
 76                 String name = e.getName();
 77                 String value = e.getTextNormalize();
 78                 List list = e.getChildren();
 79                 sb.append("<" + name + ">");
 80                 if(!list.isEmpty()) {
 81                     sb.append(XMLUtil.getChildrenText(list));
 82                 }
 83                 sb.append(value);
 84                 sb.append("</" + name + ">");
 85             }
 86         }
 87          
 88         return sb.toString();
 89     }
 90      
 91     /**
 92      * 获取xml编码字符集
 93      * @param strxml
 94      * @return
 95      * @throws IOException
 96      * @throws JDOMException
 97      */
 98     public static String getXMLEncoding(String strxml) throws JDOMException, IOException {
 99         InputStream in = HttpClientUtil.String2Inputstream(strxml);
100         SAXBuilder builder = new SAXBuilder();
101         Document doc = builder.build(in);
102         in.close();
103         return (String)doc.getProperty("encoding");
104     }
105      
106     /**
107      * 支付成功,返回微信那服务器
108      * @param return_code
109      * @param return_msg
110      * @return
111      */
112     public static String setXML(String return_code, String return_msg) {
113         return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[" + return_msg + "]]></return_msg></xml>";
114     }
115      
116     public static String createXML(Map<String,Object> map){
117         Set<Entry<String,Object>> set=map.entrySet();
118         set.iterator();
119         return null;
120     }
121      
122 }
WXUtil
 1 package com.qtkj.app.weixinpay.util;
 2 
 3 import java.util.Random;
 4 
 5 public class WXUtil {
 6     /**
 7      * 生成随机字符串
 8      * @return
 9      */
10     public static String getNonceStr() {
11         Random random = new Random();
12         return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "utf8");
13     }
14     /**
15      * 获取时间戳
16      * @return
17      */
18     public static String getTimeStamp() {
19         return String.valueOf(System.currentTimeMillis() / 1000);
20     }
21 }

  HttpClientUtil

  1 package com.qtkj.app.weixinpay.util;
  2 import java.io.ByteArrayOutputStream;
  3 import java.io.BufferedReader;
  4 import java.io.ByteArrayInputStream;
  5 import java.io.ByteArrayOutputStream;
  6 import java.io.FileInputStream;
  7 import java.io.IOException;
  8 import java.io.InputStream;
  9 import java.io.OutputStream;
 10 import java.net.HttpURLConnection;
 11 import java.net.URL;
 12 import java.security.KeyManagementException;
 13 import java.security.KeyStore;
 14 import java.security.KeyStoreException;
 15 import java.security.NoSuchAlgorithmException;
 16 import java.security.SecureRandom;
 17 import java.security.UnrecoverableKeyException;
 18 import java.security.cert.CertificateException;
 19 import java.util.HashMap;
 20 import java.util.Map;
 21 
 22 import javax.net.ssl.HttpsURLConnection;
 23 import javax.net.ssl.KeyManagerFactory;
 24 import javax.net.ssl.SSLContext;
 25 import javax.net.ssl.TrustManagerFactory;
 26 
 27 
 28 public class HttpClientUtil {
 29     /**
 30      * http客户端工具类
 31      *
 32      */
 33     public static final String SunX509 = "SunX509";
 34     public static final String JKS = "JKS";
 35     public static final String PKCS12 = "PKCS12";
 36     public static final String TLS = "TLS";
 37     
 38     /**
 39      * get HttpURLConnection
 40      * @param strUrl url地址
 41      * @return HttpURLConnection
 42      * @throws IOException
 43      */
 44     public static HttpURLConnection getHttpURLConnection(String strUrl)
 45             throws IOException {
 46         URL url = new URL(strUrl);
 47         HttpURLConnection httpURLConnection = (HttpURLConnection) url
 48                 .openConnection();
 49         return httpURLConnection;
 50     }
 51     
 52     /**
 53      * get HttpsURLConnection
 54      * @param strUrl url地址ַ
 55      * @return HttpsURLConnection
 56      * @throws IOException
 57      */
 58     public static HttpsURLConnection getHttpsURLConnection(String strUrl)
 59             throws IOException {
 60         URL url = new URL(strUrl);
 61         HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url
 62                 .openConnection();
 63         return httpsURLConnection;
 64     }
 65     
 66     /**
 67      * 获取不带查询串的url
 68      * @param strUrl
 69      * @return String
 70      */
 71     public static String getURL(String strUrl) {
 72 
 73         if(null != strUrl) {
 74             int indexOf = strUrl.indexOf("?");
 75             if(-1 != indexOf) {
 76                 return strUrl.substring(0, indexOf);
 77             } 
 78             
 79             return strUrl;
 80         }
 81         
 82         return strUrl;
 83         
 84     }
 85     
 86     /**
 87      * 获取查询串
 88      * @param strUrl
 89      * @return String
 90      */
 91     public static String getQueryString(String strUrl) {
 92         
 93         if(null != strUrl) {
 94             int indexOf = strUrl.indexOf("?");
 95             if(-1 != indexOf) {
 96                 return strUrl.substring(indexOf+1, strUrl.length());
 97             } 
 98             
 99             return "";
100         }
101         
102         return strUrl;
103     }
104     
105     /**
106      * 查询字符串转化为map
107      * name1=key1&name2=key2&...
108      * @param queryString
109      * @return
110      */
111     public static Map queryString2Map(String queryString) {
112         if(null == queryString || "".equals(queryString)) {
113             return null;
114         }
115         
116         Map m = new HashMap();
117         String[] strArray = queryString.split("&");
118         for(int index = 0; index < strArray.length; index++) {
119             String pair = strArray[index];
120             HttpClientUtil.putMapByPair(pair, m);
121         }
122         
123         return m;
124         
125     }
126     
127     /**
128      * 把键值添加到map
129      * pair:name=value
130      * @param pair name=value
131      * @param m
132      */
133     public static void putMapByPair(String pair, Map m) {
134         
135         if(null == pair || "".equals(pair)) {
136             return;
137         }
138         
139         int indexOf = pair.indexOf("=");
140         if(-1 != indexOf) {
141             String k = pair.substring(0, indexOf);
142             String v = pair.substring(indexOf+1, pair.length());
143             if(null != k && !"".equals(k)) {
144                 m.put(k, v);
145             }
146         } else {
147             m.put(pair, "");
148         }
149     }
150     /**
151      * BufferedReader转换成String<br/>
152      * 注意:流关闭需要自行处理
153      * @param reader
154      * @return
155      * @throws IOException
156      */
157     public static String bufferedReader2String(BufferedReader reader) throws IOException {
158         StringBuffer buf = new StringBuffer();
159         String line = null;
160         while( (line = reader.readLine()) != null) {
161             buf.append(line);
162             buf.append("\r\n");
163         }
164                 
165         return buf.toString();
166     }
167     /**
168      * 处理输出<br/>
169      * 注意:流关闭需要自行处理
170      * @param out
171      * @param data
172      * @param len
173      * @throws IOException
174      */
175     public static void doOutput(OutputStream out, byte[] data, int len)
176             throws IOException {
177         int dataLen = data.length;
178         int off = 0;
179         while (off < data.length) {
180             if (len >= dataLen) {
181                 out.write(data, off, dataLen);
182                 off += dataLen;
183             } else {
184                 out.write(data, off, len);
185                 off += len;
186                 dataLen -= len;
187             }
188 
189             // ˢ�»�����
190             out.flush();
191         }
192 
193     }
194     /**
195      * 获取SSLContext
196      * @param trustFile 
197      * @param trustPasswd
198      * @param keyFile
199      * @param keyPasswd
200      * @return
201      * @throws NoSuchAlgorithmException 
202      * @throws KeyStoreException 
203      * @throws IOException 
204      * @throws CertificateException 
205      * @throws UnrecoverableKeyException 
206      * @throws KeyManagementException 
207      */
208     public static SSLContext getSSLContext(
209             FileInputStream trustFileInputStream, String trustPasswd,
210             FileInputStream keyFileInputStream, String keyPasswd)
211             throws NoSuchAlgorithmException, KeyStoreException,
212             CertificateException, IOException, UnrecoverableKeyException,
213             KeyManagementException {
214 
215         // ca
216         TrustManagerFactory tmf = TrustManagerFactory.getInstance(HttpClientUtil.SunX509);
217         KeyStore trustKeyStore = KeyStore.getInstance(HttpClientUtil.JKS);
218         trustKeyStore.load(trustFileInputStream, HttpClientUtil
219                 .str2CharArray(trustPasswd));
220         tmf.init(trustKeyStore);
221 
222         final char[] kp = HttpClientUtil.str2CharArray(keyPasswd);
223         KeyManagerFactory kmf = KeyManagerFactory.getInstance(HttpClientUtil.SunX509);
224         KeyStore ks = KeyStore.getInstance(HttpClientUtil.PKCS12);
225         ks.load(keyFileInputStream, kp);
226         kmf.init(ks, kp);
227 
228         SecureRandom rand = new SecureRandom();
229         SSLContext ctx = SSLContext.getInstance(HttpClientUtil.TLS);
230         ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rand);
231 
232         return ctx;
233     }
234     
235     /**
236      * 字符串转换成char数组
237      * @param str
238      * @return char[]
239      */
240     public static char[] str2CharArray(String str) {
241         if(null == str) return null;
242         
243         return str.toCharArray();
244     }
245     
246     public static InputStream String2Inputstream(String str) {
247         return new ByteArrayInputStream(str.getBytes());
248     }
249     
250     /**
251      * InputStream转换成Byte
252      * 注意:流关闭需要自行处理
253      * @param in
254      * @return byte
255      * @throws Exception
256      */
257     public static byte[] InputStreamTOByte(InputStream in) throws IOException{  
258         
259         int BUFFER_SIZE = 4096;  
260         ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 
261         byte[] data = new byte[BUFFER_SIZE];  
262         int count = -1;  
263         
264         while((count = in.read(data,0,BUFFER_SIZE)) != -1)  
265             outStream.write(data, 0, count);  
266           
267         data = null;  
268         byte[] outByte = outStream.toByteArray();
269         outStream.close();
270         
271         return outByte;  
272     } 
273     
274     /**
275      * InputStream转换成String
276      * 注意:流关闭需要自行处理
277      * @param in
278      * @param encoding 编码
279      * @return String
280      * @throws Exception
281      */
282     public static String InputStreamTOString(InputStream in,String encoding) throws IOException{  
283 
284         return new String(InputStreamTOByte(in),encoding);
285         
286     }  
287 
288 }

 

三.控制层的接入(Controller)

  1.Controller层的代码实现过程

  1 package com.qtkj.app.weixinpay.controller;
  2 
  3 import java.io.ByteArrayOutputStream;
  4 import java.io.IOException;
  5 import java.io.InputStream;
  6 import java.io.PrintWriter;
  7 import java.io.UnsupportedEncodingException;
  8 import java.math.BigDecimal;
  9 import java.text.ParseException;
 10 import java.text.SimpleDateFormat;
 11 import java.util.Date;
 12 import java.util.HashMap;
 13 import java.util.LinkedHashMap;
 14 import java.util.Map;
 15 
 16 import javax.servlet.http.HttpServletRequest;
 17 import javax.servlet.http.HttpServletResponse;
 18 
 19 import org.jdom2.JDOMException;
 20 import org.springframework.beans.factory.annotation.Autowired;
 21 import org.springframework.stereotype.Controller;
 22 import org.springframework.ui.Model;
 23 import org.springframework.web.bind.annotation.RequestMapping;
 24 import org.springframework.web.bind.annotation.RequestMethod;
 25 import org.springframework.web.bind.annotation.RequestParam;
 26 import org.springframework.web.bind.annotation.ResponseBody;
 27 
 28 import com.qtkj.admin.common.CacheXmlConfig;
 29 import com.qtkj.admin.settings.entity.SiteConfig;
 30 import com.qtkj.admin.user.entity.User;
 31 import com.qtkj.admin.user.service.UserService;
 32 import com.qtkj.app.weixinpay.handler.ClientRequestHandler;
 33 import com.qtkj.app.weixinpay.handler.PrepayIdRequestHandler;
 34 import com.qtkj.app.weixinpay.util.ConstantUtil;
 35 import com.qtkj.app.weixinpay.util.MD5Util;
 36 import com.qtkj.app.weixinpay.util.TenpayUtil;
 37 import com.qtkj.app.weixinpay.util.UUID;
 38 import com.qtkj.app.weixinpay.util.WXUtil;
 39 import com.qtkj.app.weixinpay.util.XMLUtil;
 40 import com.qtkj.user.entity.Trade;
 41 import com.qtkj.user.service.TradeService;
 42 import com.qtkj.util.NumberUtils;
 43 import com.qtkj.util.PageHelper;
 44 
 45 
 46 /**
 47 *<p>Title:WeiXinPayController </p>
 48 *<p>Description:</p>
 49 *<p>Company:</p> 
 50 *@author ZHAO
 51 *@date 2017年10月27日
 52 */
 53 @Controller
 54 public class WeiXinPayController {
 55     
 56     @Autowired
 57     private TradeService tradeservice;
 58     
 59     @Autowired
 60     private TradeService tradeService;
 61     
 62     @Autowired
 63     private UserService userService;
 64     
 65     //------------------------------------------------------------------------------------------------------------------------------------------------------------------
 66     
 67     /**
 68     * 9.1.生成订单
 69     *@author Zhao
 70     *@date 2017年10月31日
 71     *@param request
 72     *@param response
 73     *@param model
 74     *@param legalMoney         充值法币金额(legalMoney)
 75     *@param totalPrice        充值金额(RMB)
 76     *@param userId            用户id
 77     *@return
 78     *@throws Exception
 79      */
 80     @RequestMapping("api/weixin/createOrder")  
 81     @ResponseBody
 82     public Model doWeinXinRequest(HttpServletRequest request,HttpServletResponse response,Model model,
 83             @RequestParam("totalPrice") String totalPrice, 
 84             @RequestParam("legalMoney") String legalMoney, 
 85             @RequestParam("userId") String userId
 86             ) throws Exception {  
 87         Map<String,Object> resultMap = new LinkedHashMap<>(); 
 88         try {
 89             
 90             //---------------2 生成订单号 开始------------------------  
 91             //2.1.当前时间 yyyyMMddHHmmss  
 92             String currTime = TenpayUtil.getCurrTime();  
 93             //2.2 8位日期  
 94             String strTime = currTime.substring(8, currTime.length());  
 95             //2.3四位随机数  
 96             String strRandom = TenpayUtil.buildRandom(4) + "";  
 97             //2.4 10位序列号,可以自行调整。  
 98             String strReq = strTime + strRandom;  
 99             //2.5 订单号,此处用时间加随机数生成,商户根据自己情况调整,只要保持全局唯一就行  
100             String out_trade_no = strReq;  
101             //---------------生成订单号 结束 ------------------------  
102             
103             //3.获取生成预支付订单的请求类
104             PrepayIdRequestHandler prepayReqHandler = new PrepayIdRequestHandler(request, response);
105             
106             //3.1封装数据
107             String nonce_str = WXUtil.getNonceStr();        //订单号
108             out_trade_no = String.valueOf(UUID.next());                
109             String timestamp = WXUtil.getTimeStamp();        //超时时间
110            
111             //3.2---------------------------------------------- ***** 统一下单开始 *****  -----------------------------------------------------------
112             prepayReqHandler.setParameter("appid",  ConstantUtil.APP_ID);                //平台应用appId
113             prepayReqHandler.setParameter("mch_id", ConstantUtil.MCH_ID);                //商户号
114             prepayReqHandler.setParameter("nonce_str", nonce_str);                        //随机字符串
115             prepayReqHandler.setParameter("body",   ConstantUtil.BODY);                    //商品描述
116             prepayReqHandler.setParameter("out_trade_no", out_trade_no);                //订单号
117             prepayReqHandler.setParameter("total_fee",String.valueOf(totalPrice));        //订单价格
118             prepayReqHandler.setParameter("spbill_create_ip", request.getRemoteAddr());    //获取客户端ip
119             prepayReqHandler.setParameter("notify_url", ConstantUtil.NOTIFY_URL);        //回调通知
120             prepayReqHandler.setParameter("trade_type", "APP");                            //支付类型
121             prepayReqHandler.setParameter("time_start", timestamp);                        //时间戳
122             prepayReqHandler.setGateUrl(ConstantUtil.GATEURL);                            //设置预支付id的接口url
123            
124             //3.3 注意签名(sign)的生成方式,具体见官方文档(传参都要参与生成签名,且参数名按照字典序排序,最后接上APP_KEY,转化成大写)
125             prepayReqHandler.setParameter("sign", prepayReqHandler.createMD5Sign());    //sign 签名
126             
127             //3.4 提交预支付,获取prepayid 
128             String prepayid = prepayReqHandler.sendPrepay();    
129             //---------------------------------------------- ***** 统一下单 结束 *****  --------------------------------------------------------------
130             
131             //3.5 若获取prepayid成功,将相关信息返回客户端
132             if (prepayid != null && !prepayid.equals("")) {
133                                                 
134                 
135                  //---------------4.封装订单数据开始 ------------------------  
136                                 此处用于封装你自己实体类的订单信息
137                                 
138                                 例:Trade trade = new Trade();
139                  
140                  //---------------4.封装订单数据开始 ------------------------  
141                 
142                 String signs = 
143                         "appid=" + ConstantUtil.APP_ID +
144                         "&noncestr=" + nonce_str + 
145                         "&package=Sign=WXPay"+ 
146                         "&partnerid="+ ConstantUtil.PARTNER_ID + 
147                         "&prepayid=" + prepayid + 
148                         "&timestamp=" + timestamp+
149                         "&key="+ ConstantUtil.APP_KEY;    
150                 
151                 resultMap.put("appid", ConstantUtil.APP_ID);
152                 resultMap.put("partnerid", ConstantUtil.PARTNER_ID);    //商家id
153                 resultMap.put("prepayid", prepayid);                    //预支付id
154                 resultMap.put("package", "Sign=WXPay");                  //固定常量
155                 resultMap.put("noncestr", nonce_str);                   //与请求prepayId时值一致
156                 resultMap.put("timestamp", timestamp);                  //等于请求prepayId时的time_start
157                 resultMap.put("sign", MD5Util.MD5Encode(signs, "utf8").toUpperCase());//签名方式与上面类似
158                  model.addAttribute("orderNum", out_trade_no);
159                  model.addAttribute("resultMap", resultMap);
160                 model.addAttribute("msg", "获取prepayid成功,生成订单成功");
161                 model.addAttribute("status",0);
162             }else {
163                 model.addAttribute("msg", "获取prepayid失败");
164                 model.addAttribute("status",1);
165             }
166         } catch (Exception e) {
167             model.addAttribute("msg", "订单生成失败");
168             model.addAttribute("status",2);
169         }
170         return model;
171     }
172     
173     /**
174     * 9.2 接收微信支付成功通知
175     * @param request
176     * @param response
177     * @throws IOException
178      * @throws java.io.IOException 
179      * @throws ParseException 
180     */
181     @RequestMapping(value = "api/weixin/notify")
182     public void getnotify(HttpServletRequest request, HttpServletResponse response)
183            throws IOException, ParseException {
184          System.err.println("微信支付回调");
185          System.err.println("微信支付回调");
186          //1.创建输入输出流
187          PrintWriter writer = response.getWriter();
188          InputStream inStream = request.getInputStream();
189          ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
190             byte[] buffer = new byte[1024];
191             int len = 0;
192             while ((len = inStream.read(buffer)) != -1) {
193                 outSteam.write(buffer, 0, len);
194             }
195             outSteam.close();
196             inStream.close();
197             //2.将结果转换
198             String result = new String(outSteam.toByteArray(), "utf-8");
199             System.out.println("微信支付通知结果:" + result);
200             Map<String, String> map = null;
201             try {
202                 //3.解析微信通知返回的信息
203                 map = XMLUtil.doXMLParse(result);
204                 System.err.println(map);
205             } catch (JDOMException e) {
206                 e.printStackTrace();
207             }
208             // 4.若支付成功,则告知微信服务器收到通知
209             if (map.get("return_code").equals("SUCCESS")) {
210                 if (map.get("result_code").equals("SUCCESS")) {
211                     System.out.println("充值成功!");
212                     //4.1 修改当前订单状态为:付款成功 2
213                     System.err.println("交易号:"+Long.valueOf(map.get("out_trade_no")));
214                    // Trade trade = tradeservice.selectByOrderNumber((String)(map.get("out_trade_no")));
215                     Trade trade = tradeservice.selectByTradeNumber((String)(map.get("out_trade_no")));
216                     if(trade !=null){
217                           trade.setTradeType((byte)2);                                        //设置状态
218                           trade.setPaymentTime(new Date());
219                           trade.setPayTime(new Date());
220                           if(tradeservice.updateByPrimaryKeySelective(trade) > 0){
221                               //更新成功
222                               System.err.println("通知微信后台");
223                               String notifyStr = XMLUtil.setXML("SUCCESS", "");
224                               writer.write(notifyStr);
225                               writer.flush();
226                           }
227                     }else{
228                          String notifyStr = XMLUtil.setXML("ERROR", "");
229                           writer.write(notifyStr);
230                           writer.flush();
231                     }
232                 }
233             }    
234     }
235    
236     /**
237     *微信支付成功后.通知页面
238     *@author Zhao
239     *@date 2017年11月2日
240     *@param request
241     *@return
242     *@throws UnsupportedEncodingException
243      */
244     @RequestMapping(value="api/weixin/return",method={RequestMethod.POST,RequestMethod.GET})
245     @ResponseBody
246     public Model returnUrl(@RequestParam("id") String id,HttpServletRequest request,Model model) throws UnsupportedEncodingException {  
247         System.err.println("。。。。。。 微信同步通知 。。。。。。");
248         System.err.println("。。。。。。 微信同步通知 。。。。。。");
249         System.err.println("。。。。。。 微信同步通知 。。。。。。");
250         Map  returnMap = new HashMap();  
251         try {
252             
253             Trade trade = tradeservice.selectByTradeNumber(id);
254              // 返回值Map  
255             if(trade !=null && trade.getTradeStatus() == 2){
256                 257                 User user = userService.selectByPrimaryKey(trade.gettUserId());
258                 returnMap.put("tradeType", trade.getTradeType()); //支付方式
259                 returnMap.put("phoneNum", user.getPhoneNumber()); //支付帐号
260                 returnMap.put("tradeMoney", trade.getTradeMoney()+""); //订单金额
261             }else{
262                  model.addAttribute("msg", "查询失败");
263                  model.addAttribute("status", 0);
264             }
265              model.addAttribute("returnMap", returnMap);
266              System.err.println(returnMap);
267              model.addAttribute("msg", "查询成功");
268              model.addAttribute("status", 0);
269         } catch (Exception e) {
270             model.addAttribute("msg", "查询失败");
271             model.addAttribute("status", 1);
272         }
273          
274          return model;  
275     }
276 }

  2.微信的支付注意事项

    1.在第1个接口中,生成订单时,是签名2次,将数据返回给app端的,手机端可以直接用来向微信发起支付

    2.第2个接口是app端支付成功后,微信的服务器需要回调你的这个接口,所以这个接口的地址要公网可以测,具体见上一篇的支付宝支付中的(nei wang chuan tou),这里

   还可能出现验签失败等情况,微信特别坑,就返回 -1,然后就没有提示信息了,所以希望看官不要急,先去查找一下自己的参数有没有问题,必要时可以一个一个对一遍,避免因 

   为不认真造成失误。

    3.第3个接口是在第2个接口返回给微信服务器信息后(这个信息是确认我确实收到钱了),手机端支付成功后跳转页面时所需要的数据,这个接口根据自己的业务需要把

   数据返回给手机端即可。

四.测试

  微信支付在app端支付成功后,要调用异步通知,微信官方并未提供沙箱环境,因此需要访问的url必须是外网可以访问的,在这里推荐一款内网穿透工具natapp,需要用身份证号验证,测试个支付是没问题的

   附:

    1. natapp官网: https://natapp.cn

    2.natapp 1分钟新手图文教程: https://natapp.cn/article/natapp_newbie

 

  由于本人能力水平有限,理解能为一般,有不当之处,请名位看官批评指正!!!

  

 

    

posted @ 2017-12-07 15:20  硝烟漫过十八岁  阅读(2509)  评论(6编辑  收藏  举报