Subversion Repositories SmartDukaan

Rev

Rev 37448 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
29451 manish 1
package com.smartdukaan.cron.scheduled;
2
 
36420 amit 3
import com.spice.profitmandi.common.web.client.HttpClientFactory;
30209 amit.gupta 4
import com.spice.profitmandi.dao.model.ImeiActivationTimestampModel;
5
import com.spice.profitmandi.dao.repository.fofo.ActivatedImeiRepository;
6
import com.spice.profitmandi.service.inventory.InventoryService;
36420 amit 7
import org.apache.http.NameValuePair;
8
import org.apache.http.client.entity.UrlEncodedFormEntity;
9
import org.apache.http.client.methods.CloseableHttpResponse;
10
import org.apache.http.client.methods.HttpGet;
11
import org.apache.http.client.methods.HttpPost;
12
import org.apache.http.impl.client.BasicCookieStore;
13
import org.apache.http.impl.client.CloseableHttpClient;
14
import org.apache.http.message.BasicNameValuePair;
15
import org.apache.http.util.EntityUtils;
29451 manish 16
import org.apache.logging.log4j.LogManager;
17
import org.apache.logging.log4j.Logger;
18
import org.json.JSONObject;
19
import org.springframework.beans.factory.annotation.Autowired;
20
import org.springframework.stereotype.Component;
36260 amit 21
import org.springframework.transaction.annotation.Propagation;
22
import org.springframework.transaction.annotation.Transactional;
29451 manish 23
 
36420 amit 24
import java.io.IOException;
25
import java.nio.charset.StandardCharsets;
30209 amit.gupta 26
import java.time.LocalDate;
27
import java.time.format.DateTimeFormatter;
30335 amit.gupta 28
import java.util.ArrayList;
37472 amit 29
import java.util.HashSet;
30335 amit.gupta 30
import java.util.List;
37472 amit 31
import java.util.Set;
32
import java.util.function.Predicate;
33
import java.util.stream.Collectors;
29451 manish 34
 
35
@Component
36
class VivoImeiActivationService {
37
 
33453 amit.gupta 38
    @Autowired
39
    InventoryService inventoryService;
29451 manish 40
 
33453 amit.gupta 41
    @Autowired
42
    CaptchaService captchaService;
29466 amit.gupta 43
 
33453 amit.gupta 44
    @Autowired
45
    ActivatedImeiRepository activatedImeiRepository;
29451 manish 46
 
33453 amit.gupta 47
    private static final Logger LOGGER = LogManager.getLogger(VivoImeiActivationService.class);
29451 manish 48
 
37472 amit 49
    /** Days an imei rests after an ANSWER. 0 = due again in the next day's pass. */
50
    private static final int DAYS = 0;
51
 
52
    /** Safety stop so a mis-set DAYS cannot pull an unbounded list into memory. */
53
    private static final int POOL_CAP = 20000;
54
 
55
    /**
56
     * ONE FULL PASS PER DAY over everything due, then nothing until tomorrow.
57
     *
58
     * This was two @Scheduled jobs running in parallel every 5 minutes, capped at 50
59
     * (secondary) and 10 (tertiary) imeis a tick -- so it never finished a pass over a
60
     * 9,510-row pool, and any imei whose lookup failed was retried five minutes later
61
     * rather than the next day. The pool is snapshotted here and walked to the end; it
62
     * is never re-queried mid-pass, so no imei is asked twice inside a run.
63
     *
64
     * The secondary/tertiary split is only two join paths to the same serial, so they
65
     * are concatenated into one list. distinct() is insurance: the secondary query
66
     * already excludes anything carrying a FofoLineItem.
67
     *
68
     * The 1st-of-month skip is kept -- vivo's own portal is unreliable on that day.
69
     */
36260 amit 70
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
33453 amit.gupta 71
    public void checkImeiActivation() throws Exception {
72
        if (LocalDate.now().getDayOfMonth() == 1) return;
36253 amit 73
 
37472 amit 74
        List<ImeiActivationTimestampModel> pool = new ArrayList<>();
75
        pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrand("Vivo", DAYS, POOL_CAP));
76
        pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Vivo", DAYS, POOL_CAP));
36253 amit 77
 
37472 amit 78
        List<ImeiActivationTimestampModel> imeisActivationList = pool.stream()
79
                .filter(distinctBySerial())
80
                .collect(Collectors.toList());
81
        LOGGER.info("Vivo daily pass starting: {} imeis due", imeisActivationList.size());
82
 
36253 amit 83
        processImeis(imeisActivationList);
37472 amit 84
 
85
        LOGGER.info("Vivo daily pass finished: {} imeis", imeisActivationList.size());
36253 amit 86
    }
87
 
37472 amit 88
    private static Predicate<ImeiActivationTimestampModel> distinctBySerial() {
89
        Set<String> seen = new HashSet<>();
90
        return m -> seen.add(m.getSerialNumber());
36253 amit 91
    }
92
 
93
    private void processImeis(List<ImeiActivationTimestampModel> imeisActivationList) throws Exception {
94
        if (imeisActivationList.isEmpty()) return;
95
 
36420 amit 96
        BasicCookieStore cookieStore = new BasicCookieStore();
97
        try (CloseableHttpClient httpClient = HttpClientFactory.apacheHttpWithCookies(cookieStore)) {
98
            // Seed the session cookies
99
            HttpGet seed = new HttpGet("https://www.vivo.com/in/support/IMEI");
100
            try (CloseableHttpResponse seedResponse = httpClient.execute(seed)) {
101
                EntityUtils.consumeQuietly(seedResponse.getEntity());
33453 amit.gupta 102
            }
29466 amit.gupta 103
 
36420 amit 104
            for (ImeiActivationTimestampModel imeiActivationTimestampModel : imeisActivationList) {
37408 amit 105
                // Never spend a captcha on a line item with no IMEI. Vivo answers
106
                // those with {"msg":"参数为空"} ("parameter is empty") and status 0,
107
                // which this code used to record as a captcha failure -- so a correct
108
                // code looked wrong, the row was never saved, and it came back every
109
                // 5 minutes forever. That was 76% of all rejections.
110
                String serial = imeiActivationTimestampModel.getSerialNumber();
111
                if (serial == null || serial.trim().isEmpty()) {
112
                    LOGGER.warn("Skipping activation lookup: line item has no serial number");
113
                    continue;
114
                }
115
 
37402 amit 116
                Captcha captcha = this.getNewCaptcha(httpClient, cookieStore);
117
                if (captcha == null) {
37393 amit 118
                    // Solver returned something that is not a captcha code (it was down
119
                    // for 46 days returning an nginx 502 page, which used to be submitted
120
                    // to Vivo verbatim). Skip; this IMEI is picked up on the next run.
121
                    continue;
122
                }
37407 amit 123
                this.getImeiActivation(captcha, httpClient, cookieStore, imeiActivationTimestampModel);
33453 amit.gupta 124
            }
36253 amit 125
        }
33453 amit.gupta 126
    }
29451 manish 127
 
37402 amit 128
    /** A solved captcha: the code, plus the image it came from so the verdict can be reported. */
129
    static final class Captcha {
130
        final String code;
131
        final byte[] image;
37407 amit 132
        final long fetchedAt;
29451 manish 133
 
37402 amit 134
        Captcha(String code, byte[] image) {
135
            this.code = code;
136
            this.image = image;
37407 amit 137
            this.fetchedAt = System.currentTimeMillis();
37402 amit 138
        }
139
    }
140
 
141
    public Captcha getNewCaptcha(CloseableHttpClient httpClient, BasicCookieStore cookieStore) throws Exception {
142
        HttpGet request = new HttpGet("https://www.vivo.com/in/support/generatingCodes");
143
 
144
        // Kept in memory rather than written to a shared /tmp/captcha.jpg: the two
145
        // Vivo jobs run concurrently and used to overwrite each other's image. The
146
        // bytes are needed for the verdict report anyway.
147
        byte[] bytes;
36420 amit 148
        try (CloseableHttpResponse response = httpClient.execute(request)) {
37402 amit 149
            bytes = EntityUtils.toByteArray(response.getEntity());
36253 amit 150
        }
29451 manish 151
 
37402 amit 152
        String code = captchaService.getCaptchaCode(bytes);
153
        return code == null ? null : new Captcha(code, bytes);
33453 amit.gupta 154
    }
29451 manish 155
 
37402 amit 156
    public void getImeiActivation(Captcha captcha, CloseableHttpClient httpClient,
37407 amit 157
                                  BasicCookieStore cookieStore,
33453 amit.gupta 158
                                  ImeiActivationTimestampModel vivoImeiAndActivationTimeStampModel) throws Exception {
29451 manish 159
 
33453 amit.gupta 160
        String imei = vivoImeiAndActivationTimeStampModel.getSerialNumber();
29466 amit.gupta 161
 
36420 amit 162
        List<NameValuePair> form = new ArrayList<>();
163
        form.add(new BasicNameValuePair("imei", imei));
37402 amit 164
        form.add(new BasicNameValuePair("code", captcha.code));
36420 amit 165
        HttpPost post = new HttpPost("https://www.vivo.com/in/support/checkCode");
166
        post.setEntity(new UrlEncodedFormEntity(form, StandardCharsets.UTF_8));
29466 amit.gupta 167
 
36253 amit 168
        JSONObject imeiActivationJson;
36420 amit 169
        try (CloseableHttpResponse response2 = httpClient.execute(post)) {
170
            imeiActivationJson = new JSONObject(EntityUtils.toString(response2.getEntity()));
36253 amit 171
        }
172
 
33453 amit.gupta 173
        JSONObject data = imeiActivationJson.getJSONObject("data");
174
        int status = data.getInt("status");
37402 amit 175
 
176
        // Vivo is the only judge of whether our guess was right; status 0 means the
177
        // captcha was wrong. Hand that back so the sample becomes training data.
178
        captchaService.reportVerdict(captcha.image, captcha.code, status != 0);
179
 
33453 amit.gupta 180
        if (status == 0) {
37407 amit 181
            // Vivo refuses ~56% of submissions in production, but reading the stored
182
            // images by hand showed 9 of 10 carried the CORRECT code, and no external
183
            // replication reproduces it (IMEI, user-agent, session reuse, concurrency,
184
            // source IP and submit delay were all measured and ruled out). The old log
185
            // line recorded five words and discarded the evidence, so capture it:
186
            // the code submitted, the imei, how long fetch->submit actually took inside
187
            // this process, the session cookies held at submit time, and Vivo's reply.
188
            LOGGER.info("Vivo rejected captcha code={} imei={} elapsedMs={} cookies={} response={}",
189
                    captcha.code, imei, System.currentTimeMillis() - captcha.fetchedAt,
190
                    cookieStore.getCookies(), imeiActivationJson);
33453 amit.gupta 191
            return;
192
        } else if (status == 2) {
193
            LOGGER.info("Received status 2 for {}, data {}", imei, data);
36253 amit 194
            activatedImeiRepository.saveActivation(imei, null);
33453 amit.gupta 195
            return;
196
        }
29451 manish 197
 
33453 amit.gupta 198
        JSONObject imeiQueryDto = data.getJSONObject("imeiQueryDto");
199
        LOGGER.info("imeiQueryDto - {}", imeiQueryDto);
200
        String dueTimeString = imeiQueryDto.getString("dueTime");
29466 amit.gupta 201
 
36253 amit 202
        if (dueTimeString.equals("")) {
203
            activatedImeiRepository.saveActivation(imei, null);
204
            return;
33453 amit.gupta 205
        }
29469 amit.gupta 206
 
33453 amit.gupta 207
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
208
        dueTimeString = dueTimeString.split(" ")[0];
209
        LocalDate dueDate = LocalDate.parse(dueTimeString, formatter);
30343 amit.gupta 210
 
36253 amit 211
        activatedImeiRepository.saveActivation(imei, dueDate.atStartOfDay().minusYears(1));
212
        LOGGER.info("activatedImei saved for {} with dueDate {}", imei, dueDate.atStartOfDay());
33453 amit.gupta 213
    }
36253 amit 214
}