Java實現(xiàn)微信支付的項目實踐
摘要:最近的一個項目中涉及到了支付業(yè)務(wù),其中用到了微信支付和支付寶支付,在做的過程中也遇到些問題,所以現(xiàn)在總結(jié)梳理一下,分享給有需要的人,也為自己以后回顧留個思路。
一、微信支付接入準(zhǔn)備工作:
首先,微信支付,只支持企業(yè)用戶,個人用戶是不能接入微信支付的,所以要想接入微信支付,首先需要有微信公眾號,這個的企業(yè)才能申請。有了微信公眾號,就能申請微信支付的相關(guān)內(nèi)容,所以在準(zhǔn)備開始寫代碼之前需要先把下面的這些參數(shù)申請好:公眾賬號ID、微信支付商戶號、API密鑰、AppSecret是APPID對應(yīng)的接口密碼、回調(diào)地址(回調(diào)必須保證外網(wǎng)能訪問到此地址)、發(fā)起請求的電腦IP
二、微信支付流程說明:
有了上面提到的這些參數(shù),那我們就可以接入微信支付了,下面我來看下微信支付的官方文檔(https://pay.weixin.qq.com/wiki/doc/api/index.html)、訪問該地址可以看到有多種支付方式可以選擇,我們這里選擇掃碼支付的方式(https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1)
這里我們選擇模式二,下面看下模式二的時序圖,如下圖:
模式二與模式一相比,流程更為簡單,不依賴設(shè)置的回調(diào)支付URL。商戶后臺系統(tǒng)先調(diào)用微信支付的統(tǒng)一下單接口,微信后臺系統(tǒng)返回鏈接參數(shù)code_url,商戶后臺系統(tǒng)將code_url值生成二維碼圖片,用戶使用微信客戶端掃碼后發(fā)起支付。注意:code_url有效期為2小時,過期后掃碼不能再發(fā)起支付。
三、微信支付所需Maven依賴
<!--微信支付SDK--> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.3.0</version> </dependency> <!-- json處理器:引入gson依賴 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.12</version> </dependency> <!-- 二維碼 --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.3</version> </dependency> <!-- 生成二維碼 --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.3.3</version> </dependency>
四、配置文件添加微信支付所需參數(shù)
# 微信支付相關(guān)參數(shù) wxpay: # 商戶號 mch-id: xxxxxxx # 商戶API證書序列號 mch-serial-no: xxxxxxxxxx # 商戶私鑰文件 # 注意:該文件放在項目根目錄下 private-key-path: ./apiclient_key.pem # APIv3密鑰 api-v3-key: xxxxxxxx # APPID appid: xxxxxxc27e0e7cxxx # 微信服務(wù)器地址 domain: https://api.mch.weixin.qq.com # 接收結(jié)果通知地址 # 注意:每次重新啟動ngrok,都需要根據(jù)實際情況修改這個配置 notify-domain: https://c7c1-240e-3b5-3015-be0-1bc-9bed-fca4-d09b.ngrok.io
五、微信支付下單代碼實現(xiàn)
1.Controller層
/** * native下單 */ @ApiOperation(value = "native 微信支付下單 返回Image") @GetMapping("/native") public BaseRes<String> nativePay(@RequestParam("packageId") Integer packageId) { return wxPayService.nativePay(packageId); } /** * JSAPI下單 */ @ApiOperation(value = "JSAPI微信支付下單") @GetMapping("/jsapi") public BaseRes<String> jsapiPay(@RequestParam("packageId") Integer packageId,@RequestParam("openId") String openId) { return wxPayService.jsapiPay(packageId,openId); }
注意:packageId是套餐Id,可根據(jù)情況修改
2.Service層
BaseRes<String> nativePay(Integer packageId); BaseRes<String> jsapiPay(Integer packageId, String openId);
3.實現(xiàn)層
/** * Mavicat下單 * @return * @throws Exception */ @Transactional(rollbackFor = Exception.class) @Override @SneakyThrows public BaseRes<String> nativePay(Integer packageId){ log.info("發(fā)起Navicat支付請求"); HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); CloseableHttpResponse response = wxPayExecute(packageId, null, httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//響應(yīng)體 int statusCode = response.getStatusLine().getStatusCode();//響應(yīng)狀態(tài)碼 if (statusCode == 200) { //處理成功 log.info("成功, 返回結(jié)果 = " + bodyAsString); } else if (statusCode == 204) { //處理成功,無返回Body log.info("成功"); } else { log.info("Native下單失敗,響應(yīng)碼 = " + statusCode + ",返回結(jié)果 = " + bodyAsString); throw new IOException("request failed"); } Gson gson = new Gson(); //響應(yīng)結(jié)果 Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); //二維碼 String codeUrl = resultMap.get("code_url"); return new BaseRes<>(codeUrl,ServiceCode.SUCCESS); //生成二維碼 // WxPayUtil.makeQRCode(codeUrl); } finally { response.close(); } } /** * JSAPI下單 * @return */ @Override @SneakyThrows public BaseRes<String> jsapiPay(Integer packageId, String openId) { log.info("發(fā)起Navicat支付請求"); HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType())); CloseableHttpResponse response = wxPayExecute(packageId, openId, httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//響應(yīng)體 int statusCode = response.getStatusLine().getStatusCode();//響應(yīng)狀態(tài)碼 if (statusCode == 200) { //處理成功 log.info("成功, 返回結(jié)果 = " + bodyAsString); } else if (statusCode == 204) { //處理成功,無返回Body log.info("成功"); } else { log.info("JSAPI下單失敗,響應(yīng)碼 = " + statusCode + ",返回結(jié)果 = " + bodyAsString); throw new IOException("request failed"); } Gson gson = new Gson(); //響應(yīng)結(jié)果 Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); String prepayId = resultMap.get("prepay_id"); return new BaseRes<>(prepayId,ServiceCode.SUCCESS); } finally { response.close(); } } // 封裝統(tǒng)一下單方法 private CloseableHttpResponse wxPayExecute(Integer packageId,String openId,HttpPost httpPost) throws IOException { // 獲取套餐金額 還有相關(guān)信息 ChatPackage chatPackage = chatPackageMapper.selectById(packageId); if (null == chatPackage) { throw new NingException(ServiceCode.FAILED); } BigDecimal amount = chatPackage.getAmount(); if (null == amount || amount.equals(BigDecimal.ZERO)) { throw new NingException(ServiceCode.SUCCESS); } // 從登錄信息中獲取用戶信息 TokenUser loginUserInfo = CommUtils.getLoginUserInfo(); Integer userId = loginUserInfo.getUserId(); // 請求body參數(shù) Gson gson = new Gson(); Map<String,Object> paramsMap = new HashMap<>(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); paramsMap.put("description", chatPackage.getName()); paramsMap.put("out_trade_no", WxPayUtil.generateOrderNumber(userId,packageId)); //訂單號 paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxApiType.NATIVE_NOTIFY.getType())); Map<String,Object> amountMap = new HashMap<>(); //由單位:元 轉(zhuǎn)換為單位:分,并由Bigdecimal轉(zhuǎn)換為整型 BigDecimal total = amount.multiply(new BigDecimal(100)); amountMap.put("total", total.intValue()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); // 判斷是Navicat下單還是JSAPI下單 JSAPI需要傳OPENID if (StringUtils.isNotBlank(openId)) { Map<String,Object> payerMap = new HashMap<>(); payerMap.put("openid",openId); paramsMap.put("payer",payerMap); } JSONObject attachJson = new JSONObject(); attachJson.put("packageId",packageId); attachJson.put("userId",userId); attachJson.put("total",total); paramsMap.put("attach",attachJson.toJSONString()); //將參數(shù)轉(zhuǎn)換成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("請求參數(shù) ===> {}" , jsonParams); StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成簽名并執(zhí)行請求 return wxPayClient.execute(httpPost); }
六、微信支付回調(diào)接口
1.Controller層
/** * 支付通知 * 微信支付通過支付通知接口將用戶支付成功消息通知給商戶 */ @ApiOperation(value = "支付通知", notes = "支付通知") @PostMapping("/pay/notify") @ClientAuthControl public WxRes nativeNotify() { return wxPayService.nativeNotify(); }
2.Service層
WxRes nativeNotify();
3.實現(xiàn)層
@Resource private Verifier verifier; private final ReentrantLock lock = new ReentrantLock(); @Override @SneakyThrows @Transactional public WxRes nativeNotify() { HttpServletRequest request = CommUtils.getRequest(); HttpServletResponse response = CommUtils.getResponse(); Gson gson = new Gson(); try { //處理通知參數(shù) String body = WxPayUtil.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); //簽名的驗證 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body); if (wechatPay2ValidatorForRequest.validate(request)) { throw new RuntimeException(); } log.info("通知驗簽成功"); //處理訂單 processOrder(bodyMap); return new WxRes("SUCCESS","成功"); } catch (Exception e) { e.printStackTrace(); response.setStatus(500); return new WxRes("FAIL","成功"); } } /** * 處理訂單 * * @param bodyMap */ @Transactional @SneakyThrows public void processOrder(Map<String, Object> bodyMap){ log.info("處理訂單"); //解密報文 String plainText = decryptFromResource(bodyMap); //將明文轉(zhuǎn)換成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); String attach = (String) plainTextMap.get("attach"); JSONObject attachJson = JSONObject.parseObject(attach); Integer packageId = attachJson.getInteger("packageId"); Integer userId = attachJson.getInteger("userId"); Integer total = attachJson.getInteger("total"); /*在對業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前, 要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制, 以避免函數(shù)重入造成的數(shù)據(jù)混亂*/ //嘗試獲取鎖: // 成功獲取則立即返回true,獲取失敗則立即返回false。不必一直等待鎖的釋放 if (lock.tryLock()) { try { log.info("plainText={}",plainText); //處理重復(fù)的通知 //接口調(diào)用的冪等性:無論接口被調(diào)用多少次,產(chǎn)生的結(jié)果是一致的。 String orderStatus = orderService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return; } // TODO 修改訂單狀態(tài)、添加支付記錄等 // 通知前端用戶 已完成支付 messageSocketHandle.sendMessageByUserID(userId,new TextMessage("PaySuccess")); } finally { //要主動釋放鎖 lock.unlock(); } } } /** * 對稱解密 * * @param bodyMap * @return */ @SneakyThrows private String decryptFromResource(Map<String, Object> bodyMap) { log.info("密文解密"); //通知數(shù)據(jù) Map<String, String> resourceMap = (Map) bodyMap.get("resource"); //數(shù)據(jù)密文 String ciphertext = resourceMap.get("ciphertext"); //隨機(jī)串 String nonce = resourceMap.get("nonce"); //附加數(shù)據(jù) String associatedData = resourceMap.get("associated_data"); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //數(shù)據(jù)明文 String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); return plainText; }
七、工具類和相關(guān)配置類
1.WxPayUtil工具類
@Slf4j public class WxPayUtil { private static final Random random = new Random(); // 生成訂單號 public static String generateOrderNumber(int userId, int packageId) { // 獲取當(dāng)前時間戳 long timestamp = System.currentTimeMillis(); // 生成6位隨機(jī)數(shù) int randomNum = random.nextInt(900000) + 100000; // 組裝訂單號 return String.format("%d%d%d%d", timestamp, randomNum, userId, packageId); } /** * 生成二維碼 * @param url */ public static void makeQRCode(String url){ HttpServletResponse response = CommUtils.getResponse(); //通過支付鏈接生成二維碼 HashMap<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 2); try { BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, 200, 200, hints); MatrixToImageWriter.writeToStream(bitMatrix, "PNG", response.getOutputStream()); System.out.println("創(chuàng)建二維碼完成"); } catch (Exception e) { e.printStackTrace(); } } /** * 將通知參數(shù)轉(zhuǎn)化為字符串 * * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.微信支付配置類
@Configuration @ConfigurationProperties(prefix = "wxpay") //讀取wxpay節(jié)點 @Data //使用set方法將wxpay節(jié)點中的值填充到當(dāng)前類的屬性中 @Slf4j public class WxPayConfig { // 商戶號 private String mchId; // 商戶API證書序列號 private String mchSerialNo; // 商戶私鑰文件 private String privateKeyPath; // APIv3密鑰 private String apiV3Key; // APPID private String appid; // 微信服務(wù)器地址 private String domain; // 接收結(jié)果通知地址 private String notifyDomain; /** * 獲取商戶的私鑰文件 * * @param filename * @return */ private PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私鑰文件不存在", e); } } /** * 獲取簽名驗證器 * * @return */ @Bean public ScheduledUpdateCertificatesVerifier getVerifier() { log.info("獲取簽名驗證器"); //獲取商戶私鑰 PrivateKey privateKey = getPrivateKey(privateKeyPath); //私鑰簽名對象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); //身份認(rèn)證對象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 使用定時更新的簽名驗證器,不需要傳入證書 ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } /** * 獲取http請求對象 * * @param verifier * @return */ @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) { log.info("獲取httpClient"); //獲取商戶私鑰 PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // ... 接下來,你仍然可以通過builder設(shè)置各種參數(shù),來配置你的HttpClient // 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會自動的處理簽名和驗簽,并進(jìn)行證書自動更新 CloseableHttpClient httpClient = builder.build(); return httpClient; } /** * 獲取HttpClient,無需進(jìn)行應(yīng)答簽名驗證,跳過驗簽的流程 */ @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient() { //獲取商戶私鑰 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于構(gòu)造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() //設(shè)置商戶信息 .withMerchant(mchId, mchSerialNo, privateKey) //無需進(jìn)行簽名驗證、通過withValidator((response) -> true)實現(xiàn) .withValidator((response) -> true); // 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會自動的處理簽名和驗簽,并進(jìn)行證書自動更新 CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END =="); return httpClient; } }
3.微信支付枚舉類
@AllArgsConstructor @Getter public enum WxApiType { /** * Native下單 */ NATIVE_PAY("/v3/pay/transactions/native"), /** * JSAPI下單 */ JSAPI_PAY("/v3/pay/transactions/jsapi"), /** * 查詢訂單 */ ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), /** * 關(guān)閉訂單 */ CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), /** * 支付通知 */ NATIVE_NOTIFY("/client/order/pay/notify"); /** * 類型 */ private final String type; }
4.簽名驗證類
@Slf4j public class WechatPay2ValidatorForRequest { /** * 應(yīng)答超時時間,單位為分鐘 */ protected static final long RESPONSE_EXPIRED_MINUTES = 5; protected final Verifier verifier; protected final String requestId; protected final String body; public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) { this.verifier = verifier; this.requestId = requestId; this.body = body; } protected static IllegalArgumentException parameterError(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("parameter error: " + message); } protected static IllegalArgumentException verifyFail(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("signature verify fail: " + message); } public final boolean validate(HttpServletRequest request) throws IOException { try { //處理請求參數(shù) validateParameters(request); //構(gòu)造驗簽名串 String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); //驗簽 if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, requestId); } } catch (IllegalArgumentException e) { log.error(e.getMessage()); return false; } return true; } protected final void validateParameters(HttpServletRequest request) { // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null) { throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); } } //判斷請求是否過期 String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); // 拒絕過期請求 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); } } protected final String buildMessage(HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n"; } protected final String getResponseBody(CloseableHttpResponse response) throws IOException { HttpEntity entity = response.getEntity(); return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; } }
到此這篇關(guān)于Java 實現(xiàn)微信支付的項目實踐的文章就介紹到這了,更多相關(guān)Java 微信支付內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java如何跳出當(dāng)前的多重嵌套循環(huán)的問題
Java中的循環(huán)結(jié)構(gòu)包括for循環(huán)、while循環(huán)、do-while循環(huán)和增強(qiáng)型for循環(huán),每種循環(huán)都有其適用場景,在循環(huán)中,break、continue和return分別用于跳出循環(huán)、跳過當(dāng)前循環(huán)和結(jié)束當(dāng)前方法,對于多重嵌套循環(huán)2025-01-01SpringBoot結(jié)合Mybatis實現(xiàn)創(chuàng)建數(shù)據(jù)庫表的方法
本文主要介紹了SpringBoot結(jié)合Mybatis實現(xiàn)創(chuàng)建數(shù)據(jù)庫表的方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01利用Java反射機(jī)制實現(xiàn)對象相同字段的復(fù)制操作
這篇文章主要介紹了利用Java反射機(jī)制實現(xiàn)對象相同字段的復(fù)制操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08Springboot如何實現(xiàn)自定義異常數(shù)據(jù)
這篇文章主要介紹了Springboot如何實現(xiàn)自定義異常數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09SpringCloud-Hystrix實現(xiàn)原理總結(jié)
通過hystrix可以解決雪崩效應(yīng)問題,它提供了資源隔離、降級機(jī)制、融斷、緩存等功能。接下來通過本文給大家分享SpringCloud-Hystrix實現(xiàn)原理,感興趣的朋友一起看看吧2021-05-05SpringBoot接口正確接收時間參數(shù)的幾種方式
這篇文章主要給大家介紹了關(guān)于SpringBoot接口正確接收時間參數(shù)的相關(guān)資料,文中通過代碼示例介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用springboot具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09詳解使用Spring Security OAuth 實現(xiàn)OAuth 2.0 授權(quán)
本篇文章主要介紹了詳解使用Spring Security OAuth 實現(xiàn)OAuth 2.0 授權(quán),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01