首页 > 文章列表 > java开发实现订阅到货通知帮我们买到想买的东西

java开发实现订阅到货通知帮我们买到想买的东西

java
482 2023-03-17

背景

朋友想从XX超市app购买一些物美价廉的东西,但是因为人多货少经常都是缺货的状态,订阅了到货通知也没什么效果,每次收到短信通知进入app查看的时候都没货了。最近任务做完了,闲着也是闲着就想着帮他解决这个问题。

思路

为什么每次到货通知进去看都没货呢?猜想可能有几种情况,可能这个通知并不是实时的一有货就通知,也可能是订阅的人太多了没有全部发。总之,这个到货通知不靠谱,那就只能自己实现一个到货通知了。

实现步骤:

  • 分析商品信息api
  • 定时请求商品信息api查看商品库存
  • 发送消息通知

分析商品信息api

手机wifi代理配置Charles主机地址,查看api数据,根据api名称和返回内容,可以判断接口路径是:/api/v1/xxx/goods-portal/spu/queryDetail

分析下api的返回数据内容,可以看到具体的库存信息(删除了许多没用的数据),通过名称分析可以定位到库存字段为:stockQuantity,所以我们就可以通过这个api来查看具体商品的库存数据了

{

  "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": "(粉壳鸡蛋/褐壳鸡蛋, 两种随机发货, 不影响鸡蛋品质) 精心培育 每一颗鸡蛋都可溯源 口感香醇 做法多样 懒人早餐",

    "brandId": "10194688",

    "weight": 1.5,

    "desc": "",

    "priceInfo": [

      {

        "priceType": 2,

        "price": "0",

        "priceTypeName": "原始价"

      },

      {

        "priceType": 1,

        "price": "2380",

        "priceTypeName": "销售价"

      }

    ],

    "stockInfo": {

      "stockQuantity": 68,

      "safeStockQuantity": 0,

      "soldQuantity": 0

    },

    "limitInfo": [

      {

        "limitType": 3,

        "limitNum": 5,

        "text": "限购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

}

确定完接口返回数据后,我们还要获取接口的请求数据request params(如上图所示),因为请求数据中带有商品的信息和个人的位置信息,不同的位置可能会查询到不同的仓库库存(待验证)。

定时请求商品信息api,查看商品库存

本文以Java为例,代码仅供参考和学习讨论。

获取到api信息后,我们就可以使用OkHttp或者webclient等请求工具类定时访问api,查看商品库存信息。

引入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");

                }

            }

        }

    }

    /**

     * 用于异步请求时,控制访问线程数,返回结果

     *

     * @return

     */

    private static Semaphore getSemaphoreInstance() {

        //只能1个线程同时访问

        synchronized (OkHttpUtils.class) {

            if (semaphore == null) {

                semaphore = new Semaphore(0);

            }

        }

        return semaphore;

    }

    /**

     * 创建OkHttpUtils

     *

     * @return

     */

    public static OkHttpUtils builder() {

        return new OkHttpUtils();

    }

    /**

     * 添加url

     *

     * @param url

     * @return

     */

    public OkHttpUtils url(String url) {

        this.url = url;

        return this;

    }

    /**

     * 添加参数

     *

     * @param key   参数名

     * @param value 参数值

     * @return

     */

    public OkHttpUtils addParam(String key, String value) {

        if (paramMap == null) {

            paramMap = new LinkedHashMap<>(16);

        }

        paramMap.put(key, value);

        return this;

    }

    /**

     * 添加参数

     *

     * @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;

    }

    /**

     * 添加请求头

     *

     * @param key   参数名

     * @param value 参数值

     * @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的方式提交数据,类似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;

    }

    /**

     * 同步请求

     *

     * @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 "请求失败:" + e.getMessage();

        }

    }

    /**

     * 异步请求,有返回值

     */

    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("请求出错:").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();

    }

    /**

     * 异步请求,带有接口回调

     *

     * @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添加请求头

     *

     * @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请求的证书跳过

     *

     * @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[]{};

                    }

                }

        };

    }

    /**

     * 自定义一个接口回调

     */

    public interface ICallBack {

        void onSuccessful(Call call, String data);

        void onFailure(Call call, String errorMsg);

    }

}

定时查询逻辑示例:

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")

                    // 有参数的话添加参数,可多个

                    .addParam(data)

                    // 也可以添加多个

                    .addHeader("Content-Type", "application/json; charset=utf-8")

                    // 如果是true的话,会类似于postman中post提交方式的raw,用json的方式提交,不是表单

                    // 如果是false的话传统的表单提交

                    .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() + "   库存:" + 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("有货了快来抢购!!!")

                    .context("有货了快来抢购!!!")

                    .build());

            Thread.sleep(60000);

        }

    }

}

注意点:

  • 请求不要太频繁,不要违背爬虫规则
  • 短信通知大部分是需要收费的,所以使用邮件通知

发送消息通知

本次案例使用的是qq邮件通知,qq邮箱发送需要进入设置中开启pop3服务,开启后会有一个独立密码用来发送邮件。

发送邮件工具类示例:

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协议发送电子邮件

 */ 

public class EmailUtil1 {

    // 邮箱账号 

    private final static String USERNAME = "xxx@qq.com";

    // 邮箱密码

    private final static String PASSWORD = "xxx";

    // 邮件发送协议 

    private final static String PROTOCOL = "smtp"; 

    // SMTP邮件服务器 

    private final static String HOST = "smtp.qq.com"; 

    // SMTP邮件服务器默认端口 

    private final static String PORT = "587";

    // 发件人

    private static String from = "xxx@qq.com";

    // 是否要求身份认证 

    private final static String IS_AUTH = "true"; 

    // 是否启用调试模式(启用调试模式可打印客户端与服务器交互过程时一问一答的响应消息) 

    private final static String IS_ENABLED_DEBUG_MOD = "false";

    // 收件人 

    private static String to = "aaa@qq.com";

    // 初始化连接邮件服务器的会话信息 

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

    } 

    

    /**

     * 发送简单的文本邮件

     */ 

    public static void sendTextEmail(EmailDto dto) throws Exception {

        // 创建Session实例对象 

        Session session = Session.getDefaultInstance(props); 

        // 创建MimeMessage实例对象 

        MimeMessage message = new MimeMessage(session); 

        // 设置发件人 

        message.setFrom(new InternetAddress(from)); 

        // 设置邮件主题 

        message.setSubject(dto.getSubject());

        // 设置收件人 

        message.setRecipient(RecipientType.TO, new InternetAddress(to)); 

        // 设置发送时间 

        message.setSentDate(new Date()); 

        // 设置纯文本内容为邮件正文 

        message.setText(dto.getContext());

        // 保存并生成最终的邮件内容 

        message.saveChanges();

        // 获得Transport实例对象

        Transport transport = session.getTransport(); 

        // 打开连接 

        transport.connect(USERNAME, PASSWORD); 

        // 将message对象传递给transport对象,将邮件发送出去 

        transport.sendMessage(message, message.getAllRecipients()); 

        // 关闭连接 

        transport.close(); 

    } 

    

} 

特别声明

  • 请勿将文章的任何内容用于商业或非法目的,否则后果自负。
  • 文章中涉及的任何代码,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。