java開(kāi)發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西
背景
朋友想從XX超市app購(gòu)買一些物美價(jià)廉的東西,但是因?yàn)槿硕嘭浬俳?jīng)常都是缺貨的狀態(tài),訂閱了到貨通知也沒(méi)什么效果,每次收到短信通知進(jìn)入app查看的時(shí)候都沒(méi)貨了。最近任務(wù)做完了,閑著也是閑著就想著幫他解決這個(gè)問(wèn)題。
思路
為什么每次到貨通知進(jìn)去看都沒(méi)貨呢?猜想可能有幾種情況,可能這個(gè)通知并不是實(shí)時(shí)的一有貨就通知,也可能是訂閱的人太多了沒(méi)有全部發(fā)??傊?,這個(gè)到貨通知不靠譜,那就只能自己實(shí)現(xiàn)一個(gè)到貨通知了。
實(shí)現(xiàn)步驟:
- 分析商品信息api
- 定時(shí)請(qǐng)求商品信息api查看商品庫(kù)存
- 發(fā)送消息通知
分析商品信息api
先用Charles或者Fiddler等工具分析查看商品數(shù)據(jù)時(shí)請(qǐng)求的api數(shù)據(jù),之前有寫(xiě)過(guò)Charles的具體使用方法,有興趣的同學(xué)可以看一下,這邊就不再細(xì)說(shuō)了。
手機(jī)wifi代理配置Charles主機(jī)地址,查看api數(shù)據(jù),根據(jù)api名稱和返回內(nèi)容,可以判斷接口路徑是:/api/v1/xxx/goods-portal/spu/queryDetail
分析下api的返回?cái)?shù)據(jù)內(nèi)容,可以看到具體的庫(kù)存信息(刪除了許多沒(méi)用的數(shù)據(jù)),通過(guò)名稱分析可以定位到庫(kù)存字段為:stockQuantity,所以我們就可以通過(guò)這個(gè)api來(lái)查看具體商品的庫(kù)存數(shù)據(jù)了
{ "data": { "spuId": "1277934", "hostItem": "980033855", "storeId": "6782", "title": "Member's Mark 精選鮮雞蛋 30枚裝", "masterBizType": 1, "viceBizType": 1, "categoryIdList": [ "10003023", "10003228", "10004626", "10012102" ], "isAvailable": true, "isPutOnSale": true, "sevenDaysReturn": false, "intro": "MM 精選鮮雞蛋 30枚", "subTitle": "(粉殼雞蛋/褐殼雞蛋, 兩種隨機(jī)發(fā)貨, 不影響雞蛋品質(zhì)) 精心培育 每一顆雞蛋都可溯源 口感香醇 做法多樣 懶人早餐", "brandId": "10194688", "weight": 1.5, "desc": "", "priceInfo": [ { "priceType": 2, "price": "0", "priceTypeName": "原始價(jià)" }, { "priceType": 1, "price": "2380", "priceTypeName": "銷售價(jià)" } ], "stockInfo": { "stockQuantity": 68, "safeStockQuantity": 0, "soldQuantity": 0 }, "limitInfo": [ { "limitType": 3, "limitNum": 5, "text": "限購(gòu)2件", "cycleDays": 1 } ], "deliveryAttr": 3, "favorite": false, "giveaway": false, "beltInfo": [ ], "isStoreExtent": false, "isTicket": false }, "code": "Success", "msg": "", "errorMsg": "", "traceId": "a80e1d3df8f7f216", "requestId": "54c25d584f8a4b39b95ba7bdd1331da6.182.16740102252700000", "rt": 0, "success": true }
確定完接口返回?cái)?shù)據(jù)后,我們還要獲取接口的請(qǐng)求數(shù)據(jù)request params(如上圖所示),因?yàn)檎?qǐng)求數(shù)據(jù)中帶有商品的信息和個(gè)人的位置信息,不同的位置可能會(huì)查詢到不同的倉(cāng)庫(kù)庫(kù)存(待驗(yàn)證)。
定時(shí)請(qǐng)求商品信息api,查看商品庫(kù)存
本文以Java為例,代碼僅供參考和學(xué)習(xí)討論。
獲取到api信息后,我們就可以使用OkHttp或者webclient等請(qǐng)求工具類定時(shí)訪問(wèn)api,查看商品庫(kù)存信息。
引入pom依賴
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.10.0</version> </dependency>
OkHttpUtils代碼示例:
package util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.val; import okhttp3.*; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.URLEncoder; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class OkHttpUtils { private static volatile OkHttpClient okHttpClient = null; private static volatile Semaphore semaphore = null; private Map<String, String> headerMap; private Map<String, String> paramMap; private String url; private Request.Builder request; /** * 初始化okHttpClient,并且允許https訪問(wèn) */ private OkHttpUtils() { if (okHttpClient == null) { synchronized (OkHttpUtils.class) { if (okHttpClient == null) { TrustManager[] trustManagers = buildTrustManagers(); okHttpClient = new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0]) .hostnameVerifier((hostName, session) -> true) .retryOnConnectionFailure(true) .build(); addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"); } } } } /** * 用于異步請(qǐng)求時(shí),控制訪問(wèn)線程數(shù),返回結(jié)果 * * @return */ private static Semaphore getSemaphoreInstance() { //只能1個(gè)線程同時(shí)訪問(wèn) synchronized (OkHttpUtils.class) { if (semaphore == null) { semaphore = new Semaphore(0); } } return semaphore; } /** * 創(chuàng)建OkHttpUtils * * @return */ public static OkHttpUtils builder() { return new OkHttpUtils(); } /** * 添加url * * @param url * @return */ public OkHttpUtils url(String url) { this.url = url; return this; } /** * 添加參數(shù) * * @param key 參數(shù)名 * @param value 參數(shù)值 * @return */ public OkHttpUtils addParam(String key, String value) { if (paramMap == null) { paramMap = new LinkedHashMap<>(16); } paramMap.put(key, value); return this; } /** * 添加參數(shù) * * @param data * @return */ public OkHttpUtils addParam(String data) { if (paramMap == null) { paramMap = new LinkedHashMap<>(16); } val hashMap = JSONObject.parseObject(data, HashMap.class); paramMap.putAll(hashMap); return this; } /** * 添加請(qǐng)求頭 * * @param key 參數(shù)名 * @param value 參數(shù)值 * @return */ public OkHttpUtils addHeader(String key, String value) { if (headerMap == null) { headerMap = new LinkedHashMap<>(16); } headerMap.put(key, value); return this; } /** * 初始化get方法 * * @return */ public OkHttpUtils get() { request = new Request.Builder().get(); StringBuilder urlBuilder = new StringBuilder(url); if (paramMap != null) { urlBuilder.append("?"); try { for (Map.Entry<String, String> entry : paramMap.entrySet()) { urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")). append("="). append(URLEncoder.encode(entry.getValue(), "utf-8")). append("&"); } } catch (Exception e) { e.printStackTrace(); } urlBuilder.deleteCharAt(urlBuilder.length() - 1); } request.url(urlBuilder.toString()); return this; } /** * 初始化post方法 * * @param isJsonPost true等于json的方式提交數(shù)據(jù),類似postman里post方法的raw * false等于普通的表單提交 * @return */ public OkHttpUtils post(boolean isJsonPost) { RequestBody requestBody; if (isJsonPost) { String json = ""; if (paramMap != null) { json = JSON.toJSONString(paramMap); } requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); } else { FormBody.Builder formBody = new FormBody.Builder(); if (paramMap != null) { paramMap.forEach(formBody::add); } requestBody = formBody.build(); } request = new Request.Builder().post(requestBody).url(url); return this; } /** * 同步請(qǐng)求 * * @return */ public String sync() { setHeader(request); try { Response response = okHttpClient.newCall(request.build()).execute(); assert response.body() != null; return response.body().string(); } catch (IOException e) { e.printStackTrace(); return "請(qǐng)求失?。? + e.getMessage(); } } /** * 異步請(qǐng)求,有返回值 */ public String async() { StringBuilder buffer = new StringBuilder(""); setHeader(request); okHttpClient.newCall(request.build()).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { buffer.append("請(qǐng)求出錯(cuò):").append(e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { assert response.body() != null; buffer.append(response.body().string()); getSemaphoreInstance().release(); } }); try { getSemaphoreInstance().acquire(); } catch (InterruptedException e) { e.printStackTrace(); } return buffer.toString(); } /** * 異步請(qǐng)求,帶有接口回調(diào) * * @param callBack */ public void async(ICallBack callBack) { setHeader(request); okHttpClient.newCall(request.build()).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { callBack.onFailure(call, e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { assert response.body() != null; callBack.onSuccessful(call, response.body().string()); } }); } /** * 為request添加請(qǐng)求頭 * * @param request */ private void setHeader(Request.Builder request) { if (headerMap != null) { try { for (Map.Entry<String, String> entry : headerMap.entrySet()) { request.addHeader(entry.getKey(), entry.getValue()); } } catch (Exception e) { e.printStackTrace(); } } } /** * 生成安全套接字工廠,用于https請(qǐng)求的證書(shū)跳過(guò) * * @return */ private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) { SSLSocketFactory ssfFactory = null; try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new SecureRandom()); ssfFactory = sc.getSocketFactory(); } catch (Exception e) { e.printStackTrace(); } return ssfFactory; } private static TrustManager[] buildTrustManagers() { return new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } } }; } /** * 自定義一個(gè)接口回調(diào) */ public interface ICallBack { void onSuccessful(Call call, String data); void onFailure(Call call, String errorMsg); } }
定時(shí)查詢邏輯示例:
import cn.hutool.core.date.DateUtil; import com.alibaba.fastjson.JSONObject; import entity.EmailDto; import lombok.SneakyThrows; import lombok.val; import org.junit.Test; import util.EmailUtil; import util.OkHttpUtils; /** * TODO * * @author Huangshaoyang * @date 2022-08-12 15:58:04 */ public class OkHttpTest { @Test @SneakyThrows public void t1() { // request params String data = ""; while (true) { String res = OkHttpUtils.builder().url("https://xxxx/api/v1/xxx/goods-portal/spu/queryDetail") // 有參數(shù)的話添加參數(shù),可多個(gè) .addParam(data) // 也可以添加多個(gè) .addHeader("Content-Type", "application/json; charset=utf-8") // 如果是true的話,會(huì)類似于postman中post提交方式的raw,用json的方式提交,不是表單 // 如果是false的話傳統(tǒng)的表單提交 .post(true) .sync(); // System.out.println(res); JSONObject json = JSONObject.parseObject(res); val stockQuantity = json.getJSONObject("data").getJSONObject("stockInfo").getIntValue("stockQuantity"); System.out.println(DateUtil.now() + " 庫(kù)存:" + stockQuantity); if (stockQuantity > 0 ) { sendNotify(); } else { Thread.sleep(10000); } } } @SneakyThrows private void sendNotify() { for (int i = 0; i < 3; i++) { System.out.println("send email"); EmailUtil.sendTextEmail(EmailDto.builder() .subject("有貨了快來(lái)?yè)屬?gòu)?。。?) .context("有貨了快來(lái)?yè)屬?gòu)?。?!") .build()); Thread.sleep(60000); } } }
注意點(diǎn):
- 請(qǐng)求不要太頻繁,不要違背爬蟲(chóng)規(guī)則
- 短信通知大部分是需要收費(fèi)的,所以使用郵件通知
發(fā)送消息通知
本次案例使用的是qq郵件通知,qq郵箱發(fā)送需要進(jìn)入設(shè)置中開(kāi)啟pop3服務(wù),開(kāi)啟后會(huì)有一個(gè)獨(dú)立密碼用來(lái)發(fā)送郵件。
發(fā)送郵件工具類示例:
package util; import entity.EmailDto; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.activation.FileDataSource; import javax.mail.*; import javax.mail.Message.RecipientType; import javax.mail.internet.*; import java.io.*; import java.util.Date; import java.util.Properties; /** * 使用SMTP協(xié)議發(fā)送電子郵件 */ public class EmailUtil1 { // 郵箱賬號(hào) private final static String USERNAME = "xxx@qq.com"; // 郵箱密碼 private final static String PASSWORD = "xxx"; // 郵件發(fā)送協(xié)議 private final static String PROTOCOL = "smtp"; // SMTP郵件服務(wù)器 private final static String HOST = "smtp.qq.com"; // SMTP郵件服務(wù)器默認(rèn)端口 private final static String PORT = "587"; // 發(fā)件人 private static String from = "xxx@qq.com"; // 是否要求身份認(rèn)證 private final static String IS_AUTH = "true"; // 是否啟用調(diào)試模式(啟用調(diào)試模式可打印客戶端與服務(wù)器交互過(guò)程時(shí)一問(wèn)一答的響應(yīng)消息) private final static String IS_ENABLED_DEBUG_MOD = "false"; // 收件人 private static String to = "aaa@qq.com"; // 初始化連接郵件服務(wù)器的會(huì)話信息 private static Properties props = null; static { props = new Properties(); props.setProperty("mail.transport.protocol", PROTOCOL); props.setProperty("mail.smtp.host", HOST); props.setProperty("mail.smtp.port", PORT); props.setProperty("mail.smtp.auth", IS_AUTH); props.setProperty("mail.debug",IS_ENABLED_DEBUG_MOD); // props.setProperty("mail.smtp.ssl.enable", "true"); } /** * 發(fā)送簡(jiǎn)單的文本郵件 */ public static void sendTextEmail(EmailDto dto) throws Exception { // 創(chuàng)建Session實(shí)例對(duì)象 Session session = Session.getDefaultInstance(props); // 創(chuàng)建MimeMessage實(shí)例對(duì)象 MimeMessage message = new MimeMessage(session); // 設(shè)置發(fā)件人 message.setFrom(new InternetAddress(from)); // 設(shè)置郵件主題 message.setSubject(dto.getSubject()); // 設(shè)置收件人 message.setRecipient(RecipientType.TO, new InternetAddress(to)); // 設(shè)置發(fā)送時(shí)間 message.setSentDate(new Date()); // 設(shè)置純文本內(nèi)容為郵件正文 message.setText(dto.getContext()); // 保存并生成最終的郵件內(nèi)容 message.saveChanges(); // 獲得Transport實(shí)例對(duì)象 Transport transport = session.getTransport(); // 打開(kāi)連接 transport.connect(USERNAME, PASSWORD); // 將message對(duì)象傳遞給transport對(duì)象,將郵件發(fā)送出去 transport.sendMessage(message, message.getAllRecipients()); // 關(guān)閉連接 transport.close(); } }
特別聲明
- 請(qǐng)勿將文章的任何內(nèi)容用于商業(yè)或非法目的,否則后果自負(fù)。
- 文章中涉及的任何代碼,僅用于測(cè)試和學(xué)習(xí)研究,禁止用于商業(yè)用途,不能保證其合法性,準(zhǔn)確性,完整性和有效性,請(qǐng)根據(jù)情況自行判斷。
以上就是java開(kāi)發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西的詳細(xì)內(nèi)容,更多關(guān)于java開(kāi)發(fā)到貨訂閱通知的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java設(shè)計(jì)模塊系列之書(shū)店管理系統(tǒng)單機(jī)版(三)
這篇文章主要為大家詳細(xì)介紹了Java單機(jī)版的書(shū)店管理系統(tǒng)設(shè)計(jì)模塊和思想第三章,感興趣的小伙伴們可以參考一下2016-08-08Java將Date日期類型字段轉(zhuǎn)換成json字符串的方法
這篇文章主要給大家介紹了關(guān)于Java將Date日期類型字段轉(zhuǎn)換成json字符串的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02Spring動(dòng)態(tài)多數(shù)據(jù)源配置實(shí)例Demo
本篇文章主要介紹了Spring動(dòng)態(tài)多數(shù)據(jù)源配置實(shí)例Demo,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01Java如何通過(guò)反射將map轉(zhuǎn)換為實(shí)體對(duì)象
在Java開(kāi)發(fā)中,常需要將XML配置數(shù)據(jù)轉(zhuǎn)為Map,并最終映射到實(shí)體對(duì)象上,通過(guò)單例模式管理XML轉(zhuǎn)換后的Map,并利用Java反射機(jī)制,通過(guò)屬性名稱匹配將Map的值賦給實(shí)體對(duì)象的對(duì)應(yīng)屬性,這種方法忽略了數(shù)據(jù)類型轉(zhuǎn)換,適用于數(shù)據(jù)類型一致的簡(jiǎn)單場(chǎng)景,需要類型轉(zhuǎn)換時(shí)2024-09-09Apache?SkyWalking?修復(fù)TTL?timer?失效bug詳解
這篇文章主要為大家介紹了Apache?SkyWalking?修復(fù)TTL?timer?失效bug詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之在線網(wǎng)盤(pán)系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開(kāi)發(fā)的在線網(wǎng)盤(pán)系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有網(wǎng)盤(pán)該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01Java讀取Excel、docx、pdf和txt等文件萬(wàn)能方法舉例
在Java開(kāi)發(fā)中處理文件是常見(jiàn)需求,本文以實(shí)際代碼示例詳述如何使用ApachePOI庫(kù)及其他工具讀取和寫(xiě)入Excel、Word、PDF等文件,介紹了ApachePOI、ApachePDFBox和EasyExcel等庫(kù)的使用方法,幫助開(kāi)發(fā)者有效讀取不同格式文件,需要的朋友可以參考下2024-09-09Java深入學(xué)習(xí)圖形用戶界面GUI之布局管理器
本文章向大家介紹Java GUI布局管理器,主要包括布局管理器使用實(shí)例、應(yīng)用技巧、基本知識(shí)點(diǎn)總結(jié)和需要注意事項(xiàng),具有一定的參考價(jià)值,需要的朋友可以參考一下2022-05-05