java 微信開發的工具類WeChatUtils

import com.alibaba.fastjson.JSONObject;
import com.bhudy.entity.BhudyPlugin;
import com.bhudy.service.BhudyPluginService;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.util.*;

/**
* Created by Administrator on 2019/8/23/023.
*/
@Component
public final class WeChatUtils {

@Autowired
private WeChatUtils(BhudyPluginService bhudyPluginService) {
WeChatUtils.bhudyPluginService = bhudyPluginService;
}

private static BhudyPluginService bhudyPluginService; // 獲取微信appid的接口

private static Map<String, String> tokenMap = null;

public static String wxGzhWorkOrder = "xxxxx"; // 工單狀態通知Key
public static String weGzhReport = "xxxxxx"; // 報告生成通知Key


private static String authorizationCode = "authorization_code"; // 微信調用接口使用參數authorization_code
private static String clientCredential = "client_credential"; // 微信調用接口使用參數client_credential


/**
* 發送報告生成通知
* 報告生成通知Id: P9U-LYY4qtcKKqSoDb7sfqK4GlFQvDu8G5JxWOTkUQk
* <p>
* {{first.DATA}}
* 報告類型:{{keyword1.DATA}}
* 生成時間:{{keyword2.DATA}}
* {{remark.DATA}}
*
* @return 是否發送成功
*/
public static boolean sendWeGzhReport(String openId, String first, String remark, Map<String, Object> keywordMap) {
return sendWxGzh(openId, WeChatUtils.weGzhReport, first, remark, keywordMap);
}

/**
* 發送微信公衆號工單消息提醒
* 工單模板信息id: i5JtheQBLYM9VyByYR2EqrGlbdZiiFZVyA7rndbOAuM
* <p>
* {{first.DATA}}
* 工單編號:{{keyword1.DATA}}
* 工單標題:{{keyword2.DATA}}
* 時間:{{keyword3.DATA}}
* {{remark.DATA}}
*
* @return 是否發送成功
*/
public static boolean sendWxGzhWorkOrder(String openId, String first, String remark, Map<String, Object> keywordMap) {
return sendWxGzh(openId, WeChatUtils.wxGzhWorkOrder, first, remark, keywordMap);
}

/**
* 發送微信公衆號信息
* <p>
* 參數 是否必填 說明
* touser 是 接收者openid
* template_id 是 模板ID
* url 否 模板跳轉鏈接(海外帳號沒有跳轉能力)
* miniprogram 否 跳小程序所需數據,不需跳小程序可不用傳該數據
* appid 是 所需跳轉到的小程序appid(該小程序appid必須與發模板消息的公衆號是綁定關聯關係,暫不支持小遊戲)
* pagepath 否 所需跳轉到小程序的具體頁面路徑,支持帶參數,(示例index?foo=bar),要求該小程序已發佈,暫不支持小遊戲
* data 是 模板數據
* color 否 模板內容字體顏色,不填默認爲黑色
* <p>
* json數據模板
* {"touser":"OPENID","template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY","url":"http://weixin.qq.com/download",
* "miniprogram":{"appid":"xiaochengxuappid12345","pagepath":"index?foo=bar"},"data":{"first":{"value":"恭喜你購買成功!","color":"#173177"},
* "keyword1":{"value":"巧克力","color":"#173177"},"keyword2":{"value":"39.8元","color":"#173177"},"keyword3":{"value":"2014年9月22日","color":"#173177"},
* "remark":{"value":"歡迎再次購買!","color":"#173177"}}}
*
* @param openId 接收消息的用戶openid
* @param templateId 消息模板id
* @param first 標題
* @param remark 結尾
* @param keywordMap 內容map
* @return 是否發送成功
*/
public static boolean sendWxGzh(String openId, String templateId, String first, String remark, Map<String, Object> keywordMap) {
if (openId == null || openId.equals("")) return false;

String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + WeChatUtils.getToken(BhudyPlugin.TENCENT_TAS);
Map<String, Object> map = new HashMap<>();
map.put("touser", openId);
map.put("template_id", templateId);
map.put("url", null);

Map<String, Object> miniprogramMap = new HashMap<>();


miniprogramMap.put("appid", bhudyPluginService.getBhudyPluginDataByType(BhudyPlugin.TENCENT_TAS).get(BhudyPlugin.appId));
//miniprogramMap.put("pagepath", "index");
map.put("miniprogram", miniprogramMap);

Map<String, Object> dataMap = new HashMap<>();

Map firstMap = new HashMap<>();
firstMap.put("value", first);
dataMap.put("first", firstMap);

for (Map.Entry entrySet : keywordMap.entrySet()) {
Map keyword1Map = new HashMap<>();
keyword1Map.put("value", entrySet.getValue());
dataMap.put(entrySet.getKey().toString(), keyword1Map);
}

Map remarkMap = new HashMap<>();
remarkMap.put("value", remark);
dataMap.put("remark", remarkMap);

map.put("data", dataMap);

Map<String, Object> resultMap = WeChatUtils.wxReqDataPost(url, JSONObject.toJSONString(map));
return resultMap != null;
}

/**
* 獲取用戶基本信息(UnionID機制)
* 在關注者與公衆號產生消息交互後,公衆號可獲得關注者的OpenID(加密後的微信號,每個用戶對每個公衆號的OpenID是唯一的。對於不同公衆號,同一用戶的openid不同)。公衆號可通過本接口來根據OpenID獲取用戶基本信息,包括暱稱、頭像、性別、所在城市、語言和關注時間。
* 請注意,如果開發者有在多個公衆號,或在公衆號、移動應用之間統一用戶帳號的需求,需要前往微信開放平臺(open.weixin.qq.com)綁定公衆號後,纔可利用UnionID機制來滿足上述需求。
* <p>
* 參數 是否必須 說明
* access_token 是 調用接口憑證
* openid 是 普通用戶的標識,對當前公衆號唯一
* lang 否 返回國家地區語言版本,zh_CN 簡體,zh_TW 繁體,en 英語
* ***********************************
*
* @return <br>
* 參數 說明
* subscribe 用戶是否訂閱該公衆號標識,值爲0時,代表此用戶沒有關注該公衆號,拉取不到其餘信息。
* openid 用戶的標識,對當前公衆號唯一
* nickname 用戶的暱稱
* sex 用戶的性別,值爲1時是男性,值爲2時是女性,值爲0時是未知
* city 用戶所在城市
* country 用戶所在國家
* province 用戶所在省份
* language 用戶的語言,簡體中文爲zh_CN
* headimgurl 用戶頭像,最後一個數值代表正方形頭像大小(有0、46、64、96、132數值可選,0代表640*640正方形頭像),用戶沒有頭像時該項爲空。若用戶更換頭像,原有頭像URL將失效。
* subscribe_time 用戶關注時間,爲時間戳。如果用戶曾多次關注,則取最後關注時間
* unionid 只有在用戶將公衆號綁定到微信開放平臺帳號後,纔會出現該字段。
* remark 公衆號運營者對粉絲的備註,公衆號運營者可在微信公衆平臺用戶管理界面對粉絲添加備註
* groupid 用戶所在的分組ID(兼容舊的用戶分組接口)
* tagid_list 用戶被打上的標籤ID列表
* subscribe_scene 返回用戶關注的渠道來源,ADD_SCENE_SEARCH 公衆號搜索,ADD_SCENE_ACCOUNT_MIGRATION 公衆號遷移,ADD_SCENE_PROFILE_CARD 名片分享,ADD_SCENE_QR_CODE 掃描二維碼,ADD_SCENEPROFILE LINK 圖文頁內名稱點擊,ADD_SCENE_PROFILE_ITEM 圖文頁右上角菜單,ADD_SCENE_PAID 支付後關注,ADD_SCENE_OTHERS 其他
* qr_scene 二維碼掃碼場景(開發者自定義)
* qr_scene_str 二維碼掃碼場景描述(開發者自定義)
* UnionID
*/
public static Map<String, Object> getUserInfo(String openId, Integer type) {
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + WeChatUtils.getToken(type) + "&openid=" + openId + "&lang=zh_CN";
Map resultMap = WeChatUtils.wxReqDataGet(url);
return resultMap;
}

/**
* 獲取公衆號用戶列表
* 公衆號可通過本接口來獲取帳號的關注者列表,關注者列表由一串OpenID(加密後的微信號,每個用戶對每個公衆號的OpenID是唯一的)組成。一次拉取調用最多拉取10000個關注者的OpenID,可以通過多次拉取的方式來滿足需求。
* <p>
* 參數 是否必須 說明
* access_token 是 調用接口憑證
* next_openid 是 第一個拉取的OPENID,不填默認從頭開始拉取
*
* @param nextOpenid 第一個拉取的OPENID,不填默認從頭開始拉取
* @return <br>
* 參數 說明
* total 關注該公衆賬號的總用戶數
* count 拉取的OPENID個數,最大值爲10000
* data 列表數據,OPENID的列表
* next_openid 拉取列表的最後一個用戶的OPENID
*/
public static Map<String, Object> getOpenIdListMap(String nextOpenid) {
String url = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=" + getToken(BhudyPlugin.TENCENT_TAS);
if (nextOpenid != null && nextOpenid.equals("")) {
url += "&next_openid=" + nextOpenid;
}

Map resultMap = WeChatUtils.wxReqDataGet(url);
return resultMap;
}

/**
* @param type 1是公衆號請求
* @return * * *
* 屬性 類型 說明
* access_token string 獲取到的憑證
* expires_in number 憑證有效時間,單位:秒。目前是7200秒之內的值。
* errcode number 錯誤碼
* errmsg string 錯誤信息
* <p>
* =====================================
* <p>
* errcode 的合法值
* 值 說明 最低版本
* -1 系統繁忙,此時請開發者稍候再試
* 0 請求成功
* 40001 AppSecret 錯誤或者 AppSecret 不屬於這個小程序,請開發者確認 AppSecret 的正確性
* 40002 請確保 grant_type 字段值爲 client_credential
* 40013 不合法的 AppID,請開發者檢查 AppID 的正確性,避免異常字符,注意大小寫
*/
public static String getToken(Integer type) {
Map<String, String> tokenMap = WeChatUtils.tokenMap;
// 微信的access_token有效期是7200秒,所以我們的過期時間要比微信的快token
if (tokenMap != null && Utils.formatLong(tokenMap.get("date")) >= (new Date().getTime() + (7200 * 1000))) {
return tokenMap.get("access_token");
}

Map<String, String> weChatMap = bhudyPluginService.getBhudyPluginDataByType(type);
String url = "https://api.weixin.qq.com/cgi-bin/token?appid=" + weChatMap.get(BhudyPlugin.appId) + "&secret=" + weChatMap.get(BhudyPlugin.appSecret) + "&grant_type=" + clientCredential;

Map<String, Object> resultMap = WeChatUtils.wxReqDataGet(url);
if (resultMap == null) throw new BhudyException(BhudyExceptionCode.CODE_29);

String token = (String) resultMap.get("access_token");
WeChatUtils.tokenMap = new HashMap<>();
WeChatUtils.tokenMap.put("date", String.valueOf(new Date().getTime()));
WeChatUtils.tokenMap.put("access_token", token);

return token;
}


/**
* @param code string 是 登錄時獲取的 code
* @return * * *
* 屬性 類型 說明
* openid string 用戶唯一標識
* session_key string 會話密鑰
* unionid string 用戶在開放平臺的唯一標識符,在滿足 UnionID 下發條件的情況下會返回,詳見 UnionID 機制說明。
* errcode number 錯誤碼
* errmsg string 錯誤信息
* <p>
* ===================================
* <p>
* errcode 的合法值
* 值 說明 最低版本
* -1 系統繁忙,此時請開發者稍候再試
* 0 請求成功
* 40029 code 無效
* 45011 頻率限制,每個用戶每分鐘100次
*/
public static Map<String, Object> getOpenIdAndSessionKey(String code, Integer type) {
Map<String, String> weChatMap = bhudyPluginService.getBhudyPluginDataByType(type);
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + weChatMap.get(BhudyPlugin.appId) + "&secret=" + weChatMap.get(BhudyPlugin.appSecret) + "&grant_type=" + authorizationCode + "&js_code=" + code;
return WeChatUtils.wxReqDataGet(url);
}


/**
* 微信Post請求
* 如果微信端返回錯誤碼或者沒有返回數據,這個方法直接返回null
* 該方法沒有使用線程,可能會卡死
*
* @param url 請求的url
* @param params
* @return
*/
public static Map<String, Object> wxReqDataPost(String url, String params) {
try {
Map<String, Object> reulstMap = Utils.reqPost(url, params);
if (reulstMap == null || (reulstMap.containsKey("errcode") && (Integer) reulstMap.get("errcode") != 0)) {
return null;
}
return reulstMap;
} catch (Exception e) {
return null;
}
}

/**
* 微信Get請求
* 如果微信端返回錯誤碼或者沒有返回數據,這個方法直接返回null
* 該方法沒有使用線程,可能會卡死
*
* @param url 請求的url
* @return
*/
public static Map<String, Object> wxReqDataGet(String url) {
try {
Map<String, Object> reulstMap = Utils.reqGetMap(url);
if (reulstMap == null || (reulstMap.containsKey("errcode") && (Integer) reulstMap.get("errcode") != 0)) {
return null;
}
return reulstMap;
} catch (Exception e) {
return null;
}
}


/**
* 接收事件推送
* 用法@RequestMapping(value = "/wx/api/v1/receiveEventPush.do", method = RequestMethod.POST, consumes = {"application/xml", "text/xml"}, produces = {"application/xml;charset=utf-8", "text/xml;charset=utf-8"})
* public Object receiveEventPush(@RequestBody DOMSource domSource) {
* return WxUtils.receiveEventPush(domSource);
* }
*
* @param domSource
* @return * * *
* <p>
* 目錄 MsgType Event
* 1 關注/取消關注事件
* 2 掃描帶參數二維碼事件
* 3 上報地理位置事件
* 4 自定義菜單事件
* 5 點擊菜單拉取消息時的事件推送
* 6 點擊菜單跳轉鏈接時的事件推送
* <p>
* <p>
* 》》》1.關注/取消關注事件《《《
* 參數 描述
* ToUserName 開發者微信號
* FromUserName 發送方帳號(一個OpenID)
* CreateTime 消息創建時間 (整型)
* MsgType 消息類型,event
* Event 事件類型,subscribe(訂閱)、unsubscribe(取消訂閱)
* <p>
* <p>
* 》》》2.掃描帶參數二維碼事件《《《
* 參數 描述
* ToUserName 開發者微信號
* FromUserName 發送方帳號(一個OpenID)
* CreateTime 消息創建時間 (整型)
* MsgType 消息類型,event
* Event 事件類型,subscribe
* EventKey 事件KEY值,qrscene_爲前綴,後面爲二維碼的參數值
* Ticket 二維碼的ticket,可用來換取二維碼圖片
* <p>
* <p>
* 》》》3.上報地理位置事件《《《
* 參數 描述
* ToUserName 開發者微信號
* FromUserName 發送方帳號(一個OpenID)
* CreateTime 消息創建時間 (整型)
* MsgType 消息類型,event
* Event 事件類型,LOCATION
* Latitude 地理位置緯度
* Longitude 地理位置經度
* Precision 地理位置精度
* <p>
* <p>
* 》》》5.點擊菜單拉取消息時的事件推送《《《
* 參數 描述
* ToUserName 開發者微信號
* FromUserName 發送方帳號(一個OpenID)
* CreateTime 消息創建時間 (整型)
* MsgType 消息類型,event
* Event 事件類型,CLICK
* EventKey 事件KEY值,與自定義菜單接口中KEY值對應
* <p>
* <p>
* 》》》6.點擊菜單跳轉鏈接時的事件推送《《《
* 參數 描述
* ToUserName 開發者微信號
* FromUserName 發送方帳號(一個OpenID)
* CreateTime 消息創建時間 (整型)
* MsgType 消息類型,event
* Event 事件類型,VIEW
* EventKey 事件KEY值,設置的跳轉URL
* <p>
* <p>
* MsgType
* 消息類型,接收事件推送爲event
* 消息類型,文本爲text
* 消息類型,圖片爲image
* 消息類型,語音爲voice
* 消息類型,視頻爲video
* 消息類型,音樂爲music
* 消息類型,圖文爲news
*/
public static String receiveEventPush(DOMSource domSource) {
Map<String, Object> xmlMap = domSourceToMap(domSource);

if (xmlMap == null) return "";

String msgType = (String) xmlMap.get("MsgType");
String fromUserName = (String) xmlMap.get("FromUserName");
String toUserName = (String) xmlMap.get("ToUserName");
String event = (String) xmlMap.get("Event");
String text;
if (msgType.equals("event")) {
if (event.equals("subscribe")) {
// 關注事件
text = "關注事件";
} else if (event.equals("unsubscribe")) {
// 取消關注事件
text = "取消關注事件";
} else {
text = "對不起,無法識別消息類型";
}
} else if (msgType.equals("text")) {
// 消息類型,爲text
text = "消息類型,文本爲text";
} else if (msgType.equals("image")) {
// 消息類型,圖片爲image
text = "消息類型,圖片爲image";
} else if (msgType.equals("voice")) {
// 消息類型,語音爲voice
text = "消息類型,語音爲voice";
} else if (msgType.equals("video")) {
// 消息類型,視頻爲video
text = "消息類型,視頻爲video";
} else if (msgType.equals("music")) {
// 消息類型,音樂爲music
text = "消息類型,音樂爲music";
} else if (msgType.equals("news")) {
// 消息類型,圖文爲news
text = "消息類型,圖文爲news";
} else {
text = "對不起,無法識別消息類型";
}

String returnData = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
" <xml>" +
" <Content>" + text + "</Content>" +
" <ToUserName>" + fromUserName + "</ToUserName>" +
" <FromUserName>" + toUserName + "</FromUserName>" +
" <CreateTime>" + new Date().getTime() / 1000 + "</CreateTime>" +
" <MsgType>text</MsgType>" +
" </xml>";

//輸出格式化後的json
return returnData;
}

private static Map<String, Object> domSourceToMap(DOMSource domSource) {
try {
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(domSource, result);

SAXBuilder sb = new SAXBuilder();
Document doc = sb.build(new StringReader(writer.toString()));
Element root = doc.getRootElement();

JSONObject json = new JSONObject();
json.put(root.getName(), iterateElement(root));

Map<String, Object> dataMap = JSONObject.parseObject(json.toJSONString(), Map.class);
Map<String, Object> xmlMap = (Map<String, Object>) dataMap.get("xml");
return xmlMap;
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JDOMException e) {
e.printStackTrace();
}

return null;
}

private static JSONObject iterateElement(Element element) {
List<Element> node = element.getChildren();
JSONObject obj = new JSONObject();
List list = null;
for (Element child : node) {
list = new LinkedList();
String text = child.getTextTrim();
if (text == null || text.equals("")) {
if (child.getChildren().size() == 0) {
continue;
}
if (obj.containsKey(child.getName())) {
list = (List) obj.get(child.getName());
}
list.add(iterateElement(child)); //遍歷child的子節點
obj.put(child.getName(), list);
} else {
if (obj.containsKey(child.getName())) {
Object value = obj.get(child.getName());
try {
list = (List) value;
} catch (ClassCastException e) {
list.add(value);
}
}
if (child.getChildren().size() == 0) { //child無子節點時直接設置text
obj.put(child.getName(), text);
} else {
list.add(text);
obj.put(child.getName(), list);
}
}
}
return obj;
}

/**
* 驗證微信綁定服務器的方法
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
//1.定義數組存放tooken,timestamp,nonce
String[] arr = {"7i6L5SEu4NPuYiGVAXMy0ZnFxd6", timestamp, nonce};

//2.對數組進行排序
Arrays.sort(arr);

//3.生成字符串
StringBuffer sb = new StringBuffer();
for (String s : arr) {
sb.append(s);
}

//4.sha1加密,網上均有現成代碼
String temp = getSha(sb.toString());

//5.將加密後的字符串,與微信傳來的加密簽名比較,返回結果
return temp.equals(signature);
}


public static String getSha(String str) {
if (str == null || str.length() == 0) {
return null;
}
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {

MessageDigest mdTemp = MessageDigest.getInstance("SHA1");

mdTemp.update(str.getBytes("UTF-8"));


byte[] md = mdTemp.digest();

int j = md.length;

char buf[] = new char[j * 2];

int k = 0;

for (int i = 0; i < j; i++) {

byte byte0 = md[i];

buf[k++] = hexDigits[byte0 >>> 4 & 0xf];

buf[k++] = hexDigits[byte0 & 0xf];

}

return new String(buf);

} catch (Exception e) {

// TODO: handle exception

return null;

}

}


}

微信工具類需要的其他方法 

/**
* 發送HttpPost請求
*
* @param strURL 服務地址
* @param params json字符串,例如: "{ \"id\":\"12345\" }" ;其中屬性名必須帶雙引號<br/>
* @return 成功:返回json字符串<br/>
*/
public static Map<String, Object> reqPost(String strURL, String params) {
BufferedReader reader;
try {
URL url = new URL(strURL);// 創建連接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod("POST"); // 設置請求方式
connection.setRequestProperty("Accept", "application/json"); // 設置接收數據的格式
connection.setRequestProperty("Content-Type", "application/json"); // 設置發送數據的格式
connection.connect();
//一定要用BufferedReader 來接收響應, 使用字節來接收響應的方法是接收不到內容的
OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); // utf-8編碼
out.append(params);
out.flush();
out.close();
// 讀取響應
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
String res = "";
while ((line = reader.readLine()) != null) {
res += line;
}
reader.close();

return JSON.parseObject(res, Map.class);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

/**
* 請求url
*
* @param url
* @return
*/
public static Map<String, Object> reqGetMap(String url) {
try {
URL reqURL = new URL(url);
HttpsURLConnection openConnection = (HttpsURLConnection) reqURL.openConnection();
openConnection.setConnectTimeout(10000);
//這裏我感覺獲取openid的時間比較長,不過也可能是我網絡的問題,
//所以設置的響應時間比較長
openConnection.connect();
InputStream in = openConnection.getInputStream();

StringBuilder builder = new StringBuilder();
BufferedReader bufreader = new BufferedReader(new InputStreamReader(in));
for (String temp = bufreader.readLine(); temp != null; temp = bufreader
.readLine()) {
builder.append(temp);
}

String result = builder.toString();
in.close();
openConnection.disconnect();

Map<String, Object> resultMap = JSON.parseObject(result, Map.class);

return resultMap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章