java開發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西
背景
朋友想從XX超市app購(gòu)買一些物美價(jià)廉的東西,但是因?yàn)槿硕嘭浬俳?jīng)常都是缺貨的狀態(tài),訂閱了到貨通知也沒什么效果,每次收到短信通知進(jìn)入app查看的時(shí)候都沒貨了。最近任務(wù)做完了,閑著也是閑著就想著幫他解決這個(gè)問題。
思路
為什么每次到貨通知進(jìn)去看都沒貨呢?猜想可能有幾種情況,可能這個(gè)通知并不是實(shí)時(shí)的一有貨就通知,也可能是訂閱的人太多了沒有全部發(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ù),之前有寫過Charles的具體使用方法,有興趣的同學(xué)可以看一下,這邊就不再細(xì)說了。
手機(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ù)存信息(刪除了許多沒用的數(shù)據(jù)),通過名稱分析可以定位到庫(kù)存字段為:stockQuantity,所以我們就可以通過這個(gè)api來查看具體商品的庫(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í)訪問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訪問
*/
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í),控制訪問線程數(shù),返回結(jié)果
*
* @return
*/
private static Semaphore getSemaphoreInstance() {
//只能1個(gè)線程同時(shí)訪問
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)求的證書跳過
*
* @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("有貨了快來?yè)屬?gòu)!?。?)
.context("有貨了快來?yè)屬?gòu)?。?!")
.build());
Thread.sleep(60000);
}
}
}
注意點(diǎn):
- 請(qǐng)求不要太頻繁,不要違背爬蟲規(guī)則
- 短信通知大部分是需要收費(fèi)的,所以使用郵件通知
發(fā)送消息通知
本次案例使用的是qq郵件通知,qq郵箱發(fā)送需要進(jìn)入設(shè)置中開啟pop3服務(wù),開啟后會(huì)有一個(gè)獨(dú)立密碼用來發(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ù)器交互過程時(shí)一問一答的響應(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();
// 打開連接
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開發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西的詳細(xì)內(nèi)容,更多關(guān)于java開發(fā)到貨訂閱通知的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot使用Netty實(shí)現(xiàn)遠(yuǎn)程調(diào)用的示例
這篇文章主要介紹了SpringBoot使用Netty實(shí)現(xiàn)遠(yuǎn)程調(diào)用的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
mybatis if test 不為空字符串且不為null的問題
這篇文章主要介紹了mybatis if test 不為空字符串且不為null的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
使用json字符串插入節(jié)點(diǎn)或者覆蓋節(jié)點(diǎn)
這篇文章主要介紹了使用json字符串插入節(jié)點(diǎn)或者覆蓋節(jié)點(diǎn)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
java簡(jiǎn)單網(wǎng)頁(yè)抓取的實(shí)現(xiàn)方法
這篇文章主要介紹了java簡(jiǎn)單網(wǎng)頁(yè)抓取的實(shí)現(xiàn)方法,詳細(xì)分析了與Java網(wǎng)頁(yè)抓取相關(guān)的tcp及URL相關(guān)概念,以及對(duì)應(yīng)的類文件原理,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2014-12-12
Java多線程案例實(shí)戰(zhàn)之定時(shí)器的實(shí)現(xiàn)
在Java中可以使用多線程和定時(shí)器來實(shí)現(xiàn)定時(shí)任務(wù),下面這篇文章主要給大家介紹了關(guān)于Java多線程案例之定時(shí)器實(shí)現(xiàn)的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
java 使用Scanner類接收從控制臺(tái)輸入的數(shù)據(jù)方式
這篇文章主要介紹了java 使用Scanner類接收從控制臺(tái)輸入的數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08

