今天給大家分享的關注公眾號自動推送圖文消息,以及做一個超牛逼的機器人。
先看看效果。
發錯圖了。。。這是我昨天開發的一款機器人chu了會罵人啥都不會了。
我今天將它詞庫進行了更新和升級,接入了http://www.itpk.cn/ 機器人第三詞庫
先給你截圖:
機器人的配置:
詞庫信息。可以自定義詞庫信息
來看看進一步效果
是不是乖巧多了哈哈哈。
想不想把這個乖巧機器人帶走。。。。
來吧 給你們秀一波操作。看好了xiongder們別眨眼我要開始變形了。。。
不好意思忘了一件灰常重要的事情,忘了給你們看官方API文檔了
第一步登錄微信公眾平台 現在開發-基本配置然后服務器配置。如下圖
解釋含義:
服務器地址(URL):服務器接收消息的的地址也就自己后台處理邏輯的地方
Toke:需要配置到代碼中。可以理解為密碼
消息加解密密鑰(EncodingAESKey) 就是防止別人截取你的消息,可以選擇加密 我用的明文模式
來吧走個流程圖吧。哈哈哈哈。。
直接上代碼。我代碼上的注釋很清晰。我就不多解釋了。有什么不明白隨時聯系我。。
/**
* 微信消息推送的驗證。
*
* @param request
* @param response
*/
@RequestMapping(value = "sendMsg", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
public void sendMsgget(HttpServletRequest request, HttpServletResponse response) { // 微信加密簽名
String signature = request.getParameter("signature");
// 時間戳
String timestamp = request.getParameter("timestamp");
// 隨機數
String nonce = request.getParameter("nonce");
// 隨機字符串
String echostr = request.getParameter("echostr");
PrintWriter out = null;
try {
out = response.getWriter();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 通過檢驗signature對請求進行校驗,若校驗成功則原樣返回echostr,表示接入成功,否則接入失敗
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr);
}
out.close();
out = null;
}
package cn.cnbuilder.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class SignUtil {
// 與接口配置信息中的Token要一致 需要登錄微信公眾號
private static String token = "xxxxxxxxxxx";
/**
* 驗證簽名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] { token, timestamp, nonce };
// 將token、timestamp、nonce三個參數進行字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 將三個參數字符串拼接成一個字符串進行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 將sha1加密后的字符串可與signature對比,標識該請求來源於微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 將字節數組轉換為十六進制字符串
*
* @param byteArray
* @return
*/
static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 將字節轉換為十六進制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
驗簽驗完了之后,我們開始接受消息然后處理邏輯。
/**
* 處理業務邏輯
*
* @param request
* @param response
*/
@RequestMapping(value = "sendMsg", method = RequestMethod.POST, produces = "text/html;charset=UTF-8")
public void sendMsgPost(HttpServletRequest request, HttpServletResponse response) { // 調用核心業務類接收消息、處理消息
try {
String respMessage = processRequest(request);
System.err.println(respMessage);
// 我們這里處理的是utf-8 微信要的是ios8859-1這是坑啊。。。。。
byte[] uMessage = respMessage.getBytes("UTF-8");// 編碼:字符串變成字節數組 輸入 參數(編碼表)
String iMessage = new String(uMessage, "ISO8859-1");// 解碼:字節數組變成字符串,String參數(數組,編碼表) 輸出
if (respMessage != null) {
// 響應消息
PrintWriter out = response.getWriter();
out.print(iMessage);
out.close();
}
} catch (
Exception e) {
// 也許大概不會走到這里除非異常了。。。。。。
}
}
public String processRequest(HttpServletRequest request) {
String respMessage = null;
try {
// xml請求解析
Map<String, String> requestMap = MessageUtil.parseXml(request);
// 發送方帳號(open_id)
String fromUserName = requestMap.get("FromUserName");
// 公眾帳號
String toUserName = requestMap.get("ToUserName");
// 消息類型
String msgType = requestMap.get("MsgType");
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(fromUserName);
textMessage.setFromUserName(toUserName);
textMessage.setCreateTime(new Date().getTime());
textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
// 文本消息
if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
// 接收用戶發送的文本消息內容
String content = requestMap.get("Content");
System.err.println(content);
// 創建圖文消息
NewsMessage newsMessage = new NewsMessage();
newsMessage.setToUserName(fromUserName);
newsMessage.setFromUserName(toUserName);
newsMessage.setCreateTime(new Date().getTime());
newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
List<Article> articleList = new ArrayList<Article>();
// 單圖文消息
if ("yifan".equals(content)) {
Article article = new Article();
article.setTitle("歡迎關注KingYiFan's Blog");
article.setDescription("點擊進入詳情");
article.setPicUrl(
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1521808337402&di=515fdc032be051f5085c3f9c03af5646&imgtype=0&src=http%3A%2F%2Fwww.vvfeng.com%2Fdata%2Fupload%2Fueditor%2F20170327%2F58d8c88855d9f.jpg");
article.setUrl("http://cnbuilder.cn/");
articleList.add(article);
// 設置圖文消息個數
newsMessage.setArticleCount(articleList.size());
// 設置圖文消息包含的圖文集合
newsMessage.setArticles(articleList);
// 將圖文消息對象轉換成xml字符串
respMessage = MessageUtil.newsMessageToXml(newsMessage);
}
else {
// 機器人api
String jiqiren = HttpClientUtils.sendGetUtF8("http://i.itpk.cn/api.php",
"limit=2&api_key=xxxxx&api_secret=xxxx&type=json&question="
+ content);
textMessage.setContent(jiqiren);
respMessage = MessageUtil.textMessageToXml(textMessage);
}
// 事件處理開始
} else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {
NewsMessage newsMessage = new NewsMessage();
newsMessage.setToUserName(fromUserName);
newsMessage.setFromUserName(toUserName);
newsMessage.setCreateTime(new Date().getTime());
newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
// 事件類型
String eventType = requestMap.get("Event");
List<Article> articleList = new ArrayList<Article>();
if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {
Article article = new Article();
article.setTitle("歡迎關注KingYiFan's Blog");
article.setDescription("點擊進入詳情");
article.setPicUrl(
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1521808337402&di=515fdc032be051f5085c3f9c03af5646&imgtype=0&src=http%3A%2F%2Fwww.vvfeng.com%2Fdata%2Fupload%2Fueditor%2F20170327%2F58d8c88855d9f.jpg");
article.setUrl("http://cnbuilder.cn/");
articleList.add(article);
// 設置圖文消息個數
newsMessage.setArticleCount(articleList.size());
// 設置圖文消息包含的圖文集合
newsMessage.setArticles(articleList);
// 將圖文消息對象轉換成xml字符串
respMessage = MessageUtil.newsMessageToXml(newsMessage);
try {
// 保存用戶信息
wxMsgService.savaWxInfo(fromUserName);
} catch (Exception e) {
return respMessage;
}
}
} else if (msgType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {
// 取消關注,用戶接受不到我們發送的消息了,可以在這里記錄用戶取消關注的日志信息
}
} catch (Exception e) {
e.printStackTrace();
}
return respMessage;
}
package cn.cnbuilder.utils;
import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import cn.cnbuilder.entity.wx.sendMsg.Article;
import cn.cnbuilder.entity.wx.sendMsg.MusicMessage;
import cn.cnbuilder.entity.wx.sendMsg.NewsMessage;
import cn.cnbuilder.entity.wx.sendMsg.TextMessage;
public class MessageUtil {
/**
* 返回消息類型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/**
* 返回消息類型:音樂
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/**
* 返回消息類型:圖文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
/**
* 請求消息類型:文本
*/
public static final String REQ_MESSAGE_TYPE_TEXT = "text";
/**
* 請求消息類型:圖片
*/
public static final String REQ_MESSAGE_TYPE_IMAGE = "image";
/**
* 請求消息類型:鏈接
*/
public static final String REQ_MESSAGE_TYPE_LINK = "link";
/**
* 請求消息類型:地理位置
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
/**
* 請求消息類型:音頻
*/
public static final String REQ_MESSAGE_TYPE_VOICE = "voice";
/**
* 請求消息類型:推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/**
* 事件類型:subscribe(訂閱)
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/**
* 事件類型:unsubscribe(取消訂閱)
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/**
* 事件類型:CLICK(自定義菜單點擊事件)
*/
public static final String EVENT_TYPE_CLICK = "CLICK";
/**
* 解析微信發來的請求(XML)
*
* @param request
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 將解析結果存儲在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 從request中取得輸入流
InputStream inputStream = request.getInputStream();
// 讀取輸入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子節點
List<Element> elementList = root.elements();
// 遍歷所有子節點
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 釋放資源
inputStream.close();
inputStream = null;
return map;
}
/**
* 文本消息對象轉換成xml
*
* @param textMessage 文本消息對象
* @return xml
*/
public static String textMessageToXml(TextMessage textMessage) {
xstream.alias("xml", textMessage.getClass());
return xstream.toXML(textMessage);
}
/**
* 音樂消息對象轉換成xml
*
* @param musicMessage 音樂消息對象
* @return xml
*/
public static String musicMessageToXml(MusicMessage musicMessage) {
xstream.alias("xml", musicMessage.getClass());
return xstream.toXML(musicMessage);
}
/**
* 圖文消息對象轉換成xml
*
* @param newsMessage 圖文消息對象
* @return xml
*/
public static String newsMessageToXml(NewsMessage newsMessage) {
xstream.alias("xml", newsMessage.getClass());
xstream.alias("item", new Article().getClass());
return xstream.toXML(newsMessage);
}
/**
* 擴展xstream,使其支持CDATA塊
*
* @date 2013-05-19
*/
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 對所有xml節點的轉換都增加CDATA標記
boolean cdata = true;
@SuppressWarnings("unchecked")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
}
package cn.cnbuilder.entity.wx.sendMsg;
public class TextMessage extends BaseMessage {
// 消息內容
private String Content;
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
}
package cn.cnbuilder.entity.wx.sendMsg;
import java.util.List;
public class NewsMessage extends BaseMessage{
// 圖文消息個數,限制為10條以內
private int ArticleCount;
// 多條圖文消息信息,默認第一個item為大圖
private List<Article> Articles;
public int getArticleCount() {
return ArticleCount;
}
public void setArticleCount(int articleCount) {
ArticleCount = articleCount;
}
public List<Article> getArticles() {
return Articles;
}
public void setArticles(List<Article> articles) {
Articles = articles;
}
}
package cn.cnbuilder.entity.wx.sendMsg;
public class Article {
// 圖文消息名稱
private String Title;
// 圖文消息描述
private String Description;
// 圖片鏈接,支持JPG、PNG格式,較好的效果為大圖640*320,小圖80*80,限制圖片鏈接的域名需要與開發者填寫的基本資料中的Url一致
private String PicUrl;
// 點擊圖文消息跳轉鏈接
private String Url;
public String getTitle() {
return Title;
}
public void setTitle(String title) {
Title = title;
}
public String getDescription() {
return null == Description ? "" : Description;
}
public void setDescription(String description) {
Description = description;
}
public String getPicUrl() {
return null == PicUrl ? "" : PicUrl;
}
public void setPicUrl(String picUrl) {
PicUrl = picUrl;
}
public String getUrl() {
return null == Url ? "" : Url;
}
public void setUrl(String url) {
Url = url;
}
}
package cn.cnbuilder.entity.wx.sendMsg;
public class BaseMessage {
// 開發者微信號
private String ToUserName;
// 發送方帳號(一個OpenID)
private String FromUserName;
// 消息創建時間 (整型)
private long CreateTime;
// 消息類型(text/image/location/link)
private String MsgType;
// 消息id,64位整型
private long MsgId;
public String getToUserName() {
return ToUserName;
}
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
public String getFromUserName() {
return FromUserName;
}
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
public long getCreateTime() {
return CreateTime;
}
public void setCreateTime(long createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
public long getMsgId() {
return MsgId;
}
public void setMsgId(long msgId) {
MsgId = msgId;
}
}
package cn.cnbuilder.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import net.sf.json.JSONObject;
public class HttpClientUtils {
/**
* 向指定URL發送GET方法的請求
*
* @param url 發送請求的URL
* @param param 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。
* @return URL 所代表遠程資源的響應結果
*/
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打開和URL之間的連接
URLConnection connection = realUrl.openConnection();
// 設置通用的請求屬性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
// 建立實際的連接
connection.connect();
// 獲取所有響應頭字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍歷所有的響應頭字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定義 BufferedReader輸入流來讀取URL的響應
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "gbk"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("發送GET請求出現異常!" + e);
e.printStackTrace();
}
// 使用finally塊來關閉輸入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定URL發送GET方法的請求
*
* @param url 發送請求的URL
* @param param 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。
* @return URL 所代表遠程資源的響應結果
*/
public static String sendGetUtF8(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打開和URL之間的連接
URLConnection connection = realUrl.openConnection();
// 設置通用的請求屬性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
// 建立實際的連接
connection.connect();
// 獲取所有響應頭字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍歷所有的響應頭字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定義 BufferedReader輸入流來讀取URL的響應
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("發送GET請求出現異常!" + e);
e.printStackTrace();
}
// 使用finally塊來關閉輸入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定 URL 發送POST方法的請求
*
* @param url 發送請求的 URL
* @param param 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。
* @return 所代表遠程資源的響應結果
*/
public static String sendPost(String url, String param) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打開和URL之間的連接
URLConnection conn = realUrl.openConnection();
// 設置通用的請求屬性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 發送POST請求必須設置如下兩行
conn.setDoOutput(true);
conn.setDoInput(true);
// 獲取URLConnection對象對應的輸出流
out = new PrintWriter(conn.getOutputStream());
// 發送請求參數
out.print(param);
// flush輸出流的緩沖
out.flush();
// 定義BufferedReader輸入流來讀取URL的響應
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("發送 POST 請求出現異常!" + e);
e.printStackTrace();
}
// 使用finally塊來關閉輸出流、輸入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
public static String doPostForJson(String url, String jsonParams) {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(180 * 1000)
.setConnectionRequestTimeout(180 * 1000).setSocketTimeout(180 * 1000).setRedirectsEnabled(true).build();
httpPost.setConfig(requestConfig);
httpPost.setHeader("Content-Type", "application/json"); //
try {
httpPost.setEntity(new StringEntity(jsonParams, ContentType.create("application/json", "utf-8")));
System.out.println("request parameters" + EntityUtils.toString(httpPost.getEntity()));
HttpResponse response = httpClient.execute(httpPost);
String str = EntityUtils.toString(response.getEntity());
/** 把json字符串轉換成json對象 **/
JSONObject fromObject = JSONObject.fromObject(str);
return fromObject.toString();
} catch (Exception e) {
e.printStackTrace();
return "KingYiFan溫馨提示你:post請求出異常了" + e.getMessage().toString();
} finally {
if (null != httpClient) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package cn.cnbuilder.entity.wx.sendMsg;
public class Music {
// 音樂名稱
private String Title;
// 音樂描述
private String Description;
// 音樂鏈接
private String MusicUrl;
// 高質量音樂鏈接,WIFI環境優先使用該鏈接播放音樂
private String HQMusicUrl;
public String getTitle() {
return Title;
}
public void setTitle(String title) {
Title = title;
}
public String getDescription() {
return Description;
}
public void setDescription(String description) {
Description = description;
}
public String getMusicU
package cn.cnbuilder.entity.wx.sendMsg;
public class MusicMessage {
// 音樂
private Music Music;
public Music getMusic() {
return Music;
}
public void setMusic(Music music) {
Music = music;
}
}
rl() {
return MusicUrl;
}
public void setMusicUrl(String musicUrl) {
MusicUrl = musicUrl;
}
public String getHQMusicUrl() {
return HQMusicUrl;
}
public void setHQMusicUrl(String musicUrl) {
HQMusicUrl = musicUrl;
}
}
package cn.cnbuilder.entity.wx.sendMsg;
public class MusicMessage {
// 音樂
private Music Music;
public Music getMusic() {
return Music;
}
public void setMusic(Music music) {
Music = music;
}
}
這就是微信公眾平台消息回復的教程,哪里不懂可以私信我哦!
鼓勵作者寫出更好的技術文檔,就請我喝一瓶哇哈哈哈哈哈哈哈。。
微信:
支付寶:
感謝一路支持我的人。。。。。
Love me and hold me
QQ:69673804(16年老號)
EMAIL:69673804@qq.com
友鏈交換
如果有興趣和本博客交換友鏈的話,請按照下面的格式在評論區進行評論,我會盡快添加上你的鏈接。
網站名稱:KingYiFan’S Blog
網站地址:http://blog.cnbuilder.cn
網站描述:年少是你未醒的夢話,風華是燃燼的彼岸花。
網站Logo/頭像:頭像地址