java微信小程序調(diào)用支付接口

簡介:微信小程序支付這里的坑還是有的舔株,所以提醒各位在編寫的一定要注意=嗔椤1グ丁!

1.首先呢徽千,你需要準(zhǔn)備**openid苫费,appid**,還有申請微信支付后要設(shè)置一個**32位的密鑰**双抽,需要先生成一個**sign**黍衙,得到**prepay_id**,然后再得到一個**paySign**荠诬,總之就是很墨跡琅翻,下面獻上我的controller


//微信下單支付

@ResponseBody

@RequestMapping("doOrder")

public void doOrder(HttpServletRequest request, HttpServletResponse response) throws IOException, JDOMException {

request.setCharacterEncoding("UTF-8");

response.setCharacterEncoding("UTF-8");

//得到openid

String openid = request.getParameter("openid");

int fee = 0;

//得到小程序傳過來的價格,注意這里的價格必須為整數(shù)柑贞,1代表1分方椎,所以傳過來的值必須*100;

if (null != request.getParameter("price")) {

fee = Integer.parseInt(request.getParameter("price").toString());

}

System.out.println(request.getParameter("price"));

System.out.println(fee);

//訂單編號

String did = request.getParameter("did");

//訂單標(biāo)題

String title = request.getParameter("title");

//時間戳

String times = System.currentTimeMillis() + "";

SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();

packageParams.put("appid", "wxa**********2e2");

packageParams.put("mch_id", "1486425722");

packageParams.put("nonce_str", times);//時間戳

packageParams.put("body", title);//支付主體

packageParams.put("out_trade_no", did);//編號

packageParams.put("total_fee", fee);//價格

// packageParams.put("spbill_create_ip", getIp2(request));這里之前加了ip钧嘶,但是總是獲取sign失敗棠众,原因不明,之后就注釋掉了

packageParams.put("notify_url", base+"/notify");//支付回調(diào)接口有决,用于支付成功后處理業(yè)務(wù)邏輯闸拿,小程序端支付success不能保證100%回調(diào)成功,建議采用后端異步回調(diào)處理方式书幕,回調(diào)方法在最后

packageParams.put("trade_type", "JSAPI");//這個api有新荤,固定的

packageParams.put("openid", openid);//openid

//獲取sign

String sign = PayCommonUtil.createSign("UTF-8", packageParams, "x********************************4");//最后這個是自己設(shè)置的32位密鑰

packageParams.put("sign", sign);

//轉(zhuǎn)成XML

String requestXML = PayCommonUtil.getRequestXml(packageParams);

System.out.println(requestXML);

//得到含有prepay_id的XML

String resXml = HttpUtil.postData("https://api.mch.weixin.qq.com/pay/unifiedorder", requestXML);

System.out.println(resXml);

//解析XML存入Map

Map map = XMLUtil.doXMLParse(resXml);

System.out.println(map);

// String return_code = (String) map.get("return_code");

//得到prepay_id

String prepay_id = (String) map.get("prepay_id");

SortedMap<Object, Object> packageP = new TreeMap<Object, Object>();

packageP.put("appId", "wxa**********2e2");//!Lɑ恪苛骨!注意,這里是appId,上面是appid苟呐,真懷疑寫這個東西的人痒芝。。牵素。

packageP.put("nonceStr", times);//時間戳

packageP.put("package", "prepay_id=" + prepay_id);//必須把package寫成 "prepay_id="+prepay_id這種形式

packageP.put("signType", "MD5");//paySign加密

packageP.put("timeStamp", (System.currentTimeMillis() / 1000) + "");

//得到paySign

String paySign = PayCommonUtil.createSign("UTF-8", packageP, "x********************************4");

packageP.put("paySign", paySign);

//將packageP數(shù)據(jù)返回給小程序

Gson gson = new Gson();

String json = gson.toJson(packageP);

PrintWriter pw = response.getWriter();

System.out.println(json);

pw.write(json);

pw.close();

}

```

2.下面是需要用到的工具類

(1).生成sign以及得到sign后生成XML工具類PayCommonUtil


public class PayCommonUtil {

/**

? ? * 是否簽名正確,規(guī)則是:按參數(shù)名稱a-z排序,遇到空值的參數(shù)不參加簽名严衬。

? ? * @return boolean

? ? */?

? ? public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {?

? ? ? ? StringBuffer sb = new StringBuffer();?

? ? ? ? Set es = packageParams.entrySet();?

? ? ? ? Iterator it = es.iterator();?

? ? ? ? while(it.hasNext()) {?

? ? ? ? ? ? Map.Entry entry = (Map.Entry)it.next();?

? ? ? ? ? ? String k = (String)entry.getKey();?

? ? ? ? ? ? String v = (String)entry.getValue();?

? ? ? ? ? ? if(!"sign".equals(k) && null != v && !"".equals(v)) {?

? ? ? ? ? ? ? ? sb.append(k + "=" + v + "&");?

? ? ? ? ? ? }?

? ? ? ? }?


? ? ? ? sb.append("key=" + API_KEY);?


? ? ? ? //算出摘要?

? ? ? ? String mysign = MD5.MD5Encode(sb.toString(), characterEncoding).toLowerCase();?

? ? ? ? String tenpaySign = ((String)packageParams.get("sign")).toLowerCase();?


? ? ? ? //System.out.println(tenpaySign + "? ? " + mysign);?

? ? ? ? return tenpaySign.equals(mysign);?

? ? }?


? ? /**

? ? * @author

? ? * @Description:sign簽名

? ? * @param characterEncoding

? ? *? ? ? ? ? ? 編碼格式

? ? * @param parameters

? ? *? ? ? ? ? ? 請求參數(shù)

? ? * @return

? ? */?

? ? public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {?

? ? ? ? StringBuffer sb = new StringBuffer();?

? ? ? ? Set es = packageParams.entrySet();?

? ? ? ? Iterator it = es.iterator();?

? ? ? ? while (it.hasNext()) {?

? ? ? ? ? ? Map.Entry entry = (Map.Entry) it.next();?

? ? ? ? ? ? String k = entry.getKey().toString();?

? ? ? ? ? ? String v = entry.getValue().toString();?

? ? ? ? ? ? if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {?

? ? ? ? ? ? ? ? sb.append(k + "=" + v + "&");?

? ? ? ? ? ? }?

? ? ? ? }?

? ? ? ? sb.append("key=" + API_KEY);?

? ? ? ? String sign = MD5.MD5Encode(sb.toString(), characterEncoding).toUpperCase();?

? ? ? ? return sign;?

? ? }?


? ? /**

? ? * @author

? ? * @Description:將請求參數(shù)轉(zhuǎn)換為xml格式的string

? ? * @param parameters

? ? *? ? ? ? ? ? 請求參數(shù)

? ? * @return

? ? */?

? ? public static String getRequestXml(SortedMap<Object, Object> parameters) {?

? ? ? ? StringBuffer sb = new StringBuffer();?

? ? ? ? sb.append("<xml>");?

? ? ? ? Set es = parameters.entrySet();?

? ? ? ? Iterator it = es.iterator();?

? ? ? ? while (it.hasNext()) {?

? ? ? ? ? ? Map.Entry entry = (Map.Entry) it.next();?

? ? ? ? ? ? String k = entry.getKey().toString();?

? ? ? ? ? ? String v = entry.getValue().toString();?

? ? ? ? ? ? if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {?

? ? ? ? ? ? ? ? sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");?

? ? ? ? ? ? } else {?

? ? ? ? ? ? ? ? sb.append("<" + k + ">" + v + "</" + k + ">");?

? ? ? ? ? ? }?

? ? ? ? }?

? ? ? ? sb.append("</xml>");?

? ? ? ? return sb.toString();?

? ? }?


? ? /**

? ? * 取出一個指定長度大小的隨機正整數(shù).

? ? *?

? ? * @param length

? ? *? ? ? ? ? ? int 設(shè)定所取出隨機數(shù)的長度。length小于11

? ? * @return int 返回生成的隨機數(shù)笆呆。

? ? */?

? ? 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));?

? ? }?


? ? /**

? ? * 獲取當(dāng)前時間 yyyyMMddHHmmss

? ? *?

? ? * @return String

? ? */?

? ? public static String getCurrTime() {?

? ? ? ? Date now = new Date();?

? ? ? ? SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");?

? ? ? ? String s = outFormat.format(now);?

? ? ? ? return s;?

? ? }?

}

```

(2).訪問官方接口得到含有prepay_id的XML工具類HttpUtil


public class HttpUtil {

//private static final Log logger = Logs.get();?

? ? private final static int CONNECT_TIMEOUT = 5000; // in milliseconds?

? ? private final static String DEFAULT_ENCODING = "UTF-8";?


? ? public static String postData(String urlStr, String data){?

? ? ? ? return postData(urlStr, data, null);?

? ? }?


? ? public static String postData(String urlStr, String data, String contentType){?

? ? ? ? BufferedReader reader = null;?

? ? ? ? try {?

? ? ? ? ? ? URL url = new URL(urlStr);?

? ? ? ? ? ? URLConnection conn = url.openConnection();?

? ? ? ? ? ? conn.setDoOutput(true);?

? ? ? ? ? ? conn.setConnectTimeout(CONNECT_TIMEOUT);?

? ? ? ? ? ? conn.setReadTimeout(CONNECT_TIMEOUT);?

? ? ? ? ? ? if(contentType != null)?

? ? ? ? ? ? ? ? conn.setRequestProperty("content-type", contentType);?

? ? ? ? ? ? OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);?

? ? ? ? ? ? if(data == null)?

? ? ? ? ? ? ? ? data = "";?

? ? ? ? ? ? writer.write(data);?

? ? ? ? ? ? writer.flush();?

? ? ? ? ? ? writer.close();? ?


? ? ? ? ? ? reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));?

? ? ? ? ? ? StringBuilder sb = new StringBuilder();?

? ? ? ? ? ? String line = null;?

? ? ? ? ? ? while ((line = reader.readLine()) != null) {?

? ? ? ? ? ? ? ? sb.append(line);?

? ? ? ? ? ? ? ? sb.append("\r\n");?

? ? ? ? ? ? }?

? ? ? ? ? ? return sb.toString();?

? ? ? ? } catch (IOException e) {?

? ? ? ? ? ? //logger.error("Error connecting to " + urlStr + ": " + e.getMessage());?

? ? ? ? } finally {?

? ? ? ? ? ? try {?

? ? ? ? ? ? ? ? if (reader != null)?

? ? ? ? ? ? ? ? ? ? reader.close();?

? ? ? ? ? ? } catch (IOException e) {?

? ? ? ? ? ? }?

? ? ? ? }?

? ? ? ? return null;?

? ? }?

}

```

(3).解析XML工具類


public class XMLUtil {

public static Map doXMLParse(String strxml) throws JDOMException, IOException {?

? ? ? ? strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");?


? ? ? ? if(null == strxml || "".equals(strxml)) {?

? ? ? ? ? ? return null;?

? ? ? ? }?


? ? ? ? Map m = new HashMap();?


? ? ? ? InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));?

? ? ? ? SAXBuilder builder = new SAXBuilder();?

? ? ? ? Document doc = builder.build(in);?

? ? ? ? Element root = doc.getRootElement();?

? ? ? ? List list = root.getChildren();?

? ? ? ? Iterator it = list.iterator();?

? ? ? ? while(it.hasNext()) {?

? ? ? ? ? ? Element e = (Element) it.next();?

? ? ? ? ? ? String k = e.getName();?

? ? ? ? ? ? String v = "";?

? ? ? ? ? ? List children = e.getChildren();?

? ? ? ? ? ? if(children.isEmpty()) {?

? ? ? ? ? ? ? ? v = e.getTextNormalize();?

? ? ? ? ? ? } else {?

? ? ? ? ? ? ? ? v = XMLUtil.getChildrenText(children);?

? ? ? ? ? ? }?


? ? ? ? ? ? m.put(k, v);?

? ? ? ? }?


? ? ? ? //關(guān)閉流?

? ? ? ? in.close();?


? ? ? ? return m;?

? ? }?


? ? /**

? ? * 獲取子結(jié)點的xml

? ? * @param children

? ? * @return String

? ? */?

? ? 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();?

? ? }?

}

```

(4).MD5加密工具類


public class MD5 {

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];

? ? }

? ? 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 final String hexDigits[] = { "0", "1", "2", "3", "4", "5",

? ? ? ? "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };

}

```

3.小程序支付函數(shù)


gopay: function () {

? ? var that = this

? ? wx.request({

? ? ? url: app.baseurl + 'doOrder',

? ? ? data: {

? ? ? ? 'openid': wx.getStorageSync('openids'),

? ? ? ? 'title': that.data.title,

? ? ? ? 'did': that.data.did,

? ? ? ? 'price': that.data.price*100

? ? ? },

? ? ? method: 'POST',

? ? ? header: {

? ? ? ? "content-type": 'application/x-www-form-urlencoded'

? ? ? },

? ? ? success: function (res) {

? ? ? ? console.log(res.data)

? ? ? ? console.log(res.data.timeStamp)

? ? ? ? console.log(res.data.nonceStr)

? ? ? ? console.log(res.data.package)

? ? ? ? console.log(res.data.paySign)

? ? ? ? wx.requestPayment({

? ? ? ? ? timeStamp: res.data.timeStamp,

? ? ? ? ? nonceStr: res.data.nonceStr,

? ? ? ? ? package: res.data.package,

? ? ? ? ? signType: res.data.signType,

? ? ? ? ? paySign: res.data.paySign,

? ? ? ? ? success: function (res) {

? ? ? ? ? console.log('支付調(diào)用成功',res)

? ? ? ? ? },

? ? ? ? ? fail: function (res) {

? ? ? ? ? ? console.log(res)

? ? ? ? ? }

? ? ? ? })

? ? ? }

? ? })

? }

```

4请琳、支付成功回調(diào)


/**

? ? * 此函數(shù)會被執(zhí)行多次,如果支付狀態(tài)已經(jīng)修改為已支付腰奋,則下次再調(diào)的時候判斷是否已經(jīng)支付单起,如果已經(jīng)支付了,則什么也執(zhí)行

? ? * @param request

? ? * @param response

? ? * @return

? ? * @throws IOException

? ? * @throws JDOMException

? ? */

? ? @RequestMapping(value = "notify")

? ? @ResponseBody

? ? public String notify(HttpServletRequest request, HttpServletResponse response) throws IOException {

? ? String resXml = "";

? ? ? ? System.out.println("微信支付回調(diào)");

//resultxml中含用戶訂單號等信息劣坊,解析后用于處理訂單

? ? ? ? Map<String, String> params = PayCommonUtil.doXMLParse(resultxml);

? ? ? ? if (!PayCommonUtil.isTenpaySign(params)) {

? ? ? ? ? ? // 支付失敗

? ? ? ? ? ? resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"

? ? ? ? ? ? ? ? ? ? + "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> ";

? ? ? ? } else {

? ? ? ? ? ? System.out.println("==========付款成功==========");

? ? ? ? ? ? // ------------------------------

? ? ? ? ? ? // 處理業(yè)務(wù)開始

? ? ? ? ? ? // ------------------------------

? ? ? ? ? ? // 此處處理訂單狀態(tài)嘀倒,結(jié)合自己的訂單數(shù)據(jù)完成訂單狀態(tài)的更新

? ? ? ? ? ? // ------------------------------

//通知微信.異步確認成功.必寫.不然會一直通知后臺.八次之后就認為交易失敗了

resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"

? ? ? ? ? ? ? ? ? ? + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";

? ? ? ? }

? ? ? ? BufferedOutputStream out = new BufferedOutputStream(

? ? ? ? ? ? ? ? response.getOutputStream());

? ? ? ? out.write(resXml.getBytes());

? ? ? ? out.flush();

? ? ? ? out.close();

? ? }

```

結(jié)束啦,小程序支付的java后臺就這些局冰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末测蘑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子康二,更是在濱河造成了極大的恐慌碳胳,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沫勿,死亡現(xiàn)場離奇詭異挨约,居然都是意外死亡味混,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門诫惭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翁锡,“玉大人,你說我怎么就攤上這事夕土」菹危” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵怨绣,是天一觀的道長角溃。 經(jīng)常有香客問我,道長篮撑,這世上最難降的妖魔是什么减细? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮咽扇,結(jié)果婚禮上邪财,老公的妹妹穿的比我還像新娘。我一直安慰自己质欲,他們只是感情好树埠,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嘶伟,像睡著了一般怎憋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上九昧,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天绊袋,我揣著相機與錄音,去河邊找鬼铸鹰。 笑死癌别,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蹋笼。 我是一名探鬼主播展姐,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼剖毯!你這毒婦竟也來了圾笨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤逊谋,失蹤者是張志新(化名)和其女友劉穎擂达,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胶滋,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡板鬓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年悲敷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片穗熬。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡镀迂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唤蔗,到底是詐尸還是另有隱情,我是刑警寧澤窟赏,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布妓柜,位于F島的核電站,受9級特大地震影響涯穷,放射性物質(zhì)發(fā)生泄漏棍掐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一拷况、第九天 我趴在偏房一處隱蔽的房頂上張望作煌。 院中可真熱鬧,春花似錦赚瘦、人聲如沸粟誓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹰服。三九已至,卻和暖如春揽咕,著一層夾襖步出監(jiān)牢的瞬間悲酷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工亲善, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留设易,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓蛹头,卻偏偏與公主長得像顿肺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掘而,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350