|
|
@@ -7,6 +7,7 @@ import com.jzg.commons.entity.quote.vo.aggregated.*;
|
|
|
import com.jzg.commons.entity.quote.vo.base.LoginBase;
|
|
|
import com.jzg.commons.entity.quote.vo.image.InsuranceImageVo;
|
|
|
import com.jzg.commons.exception.SystemException;
|
|
|
+import com.jzg.commons.util.AESUtils;
|
|
|
import com.jzg.quotation.commons.annotation.QuoteCrawlerService;
|
|
|
import com.jzg.quotation.commons.constant.InsuranceCode;
|
|
|
import com.jzg.quotation.commons.entity.DetectLoginRequest;
|
|
|
@@ -30,13 +31,18 @@ import org.redisson.api.RMap;
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
+import javax.imageio.ImageIO;
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.ArrayList;
|
|
|
+import java.util.Base64;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Objects;
|
|
|
+import java.util.UUID;
|
|
|
|
|
|
/**
|
|
|
* 人保车险爬虫服务实现 *
|
|
|
@@ -149,9 +155,36 @@ public class RenBaoNewCrawlerRequestImpl implements RenBaoNewCrawlerRequest {
|
|
|
LoginInfoBO loginInfo, String username) {
|
|
|
int modelCodeRetryCount = 0;
|
|
|
int duplicateInsuranceRetryCount = 0;
|
|
|
+ int hkcs = 0; // 滑块风控重试计数器
|
|
|
for (int retry = 0; retry < 6; retry++) {
|
|
|
RenBaoNewCrawlerQuoteResponse response = requestComponent.quote(request, headers);
|
|
|
|
|
|
+ // 判断触发滑块风控
|
|
|
+ if (response.getStatus() != null && response.getStatus() == -2) {
|
|
|
+ boolean passSpilderMainRes = false;
|
|
|
+ // 对应Python: validate0 = caculatePremium_res['statusText']; validate1 = '/bizSave/caculatePremium'
|
|
|
+ String validate0 = response.getStatusText();
|
|
|
+ String validate1 = "/bizSave/caculatePremium";
|
|
|
+ log.error("检测到滑块: statusText={}, {}", validate0, JSONUtil.toJsonStr(response));
|
|
|
+ if (hkcs < 2) {
|
|
|
+ while (hkcs < 2) {
|
|
|
+ hkcs++;
|
|
|
+ passSpilderMainRes = passSpilderMain(headers, username, validate0, validate1);
|
|
|
+ if (passSpilderMainRes) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (passSpilderMainRes) {
|
|
|
+ log.info("滑块验证通过,重新发起报价");
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ throw new SystemException("触发滑块风控,未通过,请稍后报价");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new SystemException("触发滑块风控,未通过,请稍后报价");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (isSuccess(response)) {
|
|
|
return response;
|
|
|
}
|
|
|
@@ -463,6 +496,221 @@ public class RenBaoNewCrawlerRequestImpl implements RenBaoNewCrawlerRequest {
|
|
|
return "未知错误";
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 滑块验证主程序(对应Python的pass_spilder_main方法)
|
|
|
+ *
|
|
|
+ * <p>获取滑块图片、计算滑块位置、提交验证,通过后还需二次验证</p>
|
|
|
+ *
|
|
|
+ * @param headers 请求头
|
|
|
+ * @param username 用户名
|
|
|
+ * @param validate0 来自报价响应的statusText(对应Python: caculatePremium_res['statusText'])
|
|
|
+ * @param validate1 触发滑块的接口路径(对应Python: '/bizSave/caculatePremium')
|
|
|
+ * @return true-滑块通过,false-未通过
|
|
|
+ */
|
|
|
+ private boolean passSpilderMain(HttpHeaders headers, String username, String validate0, String validate1) {
|
|
|
+ try {
|
|
|
+ // 1. 获取滑块验证信息(对应Python的get_spilder)
|
|
|
+ Map<String, Object> spilderInfo = requestComponent.getSpilder(headers);
|
|
|
+
|
|
|
+ // 检查响应是否有效
|
|
|
+ if (spilderInfo == null || !spilderInfo.containsKey("repData")) {
|
|
|
+ log.error("获取滑块信息失败: {}", JSONUtil.toJsonStr(spilderInfo));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> repData = (Map<String, Object>) spilderInfo.get("repData");
|
|
|
+ if (repData == null) {
|
|
|
+ log.error("滑块信息repData为空");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ String jigsawImageBase64 = (String) repData.get("jigsawImageBase64");
|
|
|
+ String originalImageBase64 = (String) repData.get("originalImageBase64");
|
|
|
+ String secretKey = (String) repData.get("secretKey");
|
|
|
+ String token = (String) repData.get("token");
|
|
|
+
|
|
|
+ if (jigsawImageBase64 == null || originalImageBase64 == null
|
|
|
+ || secretKey == null || token == null) {
|
|
|
+ log.error("滑块信息不完整: jigsaw={}, original={}, secretKey={}, token={}",
|
|
|
+ jigsawImageBase64 != null, originalImageBase64 != null,
|
|
|
+ secretKey != null, token != null);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 解码图片
|
|
|
+ byte[] targetBytes = Base64.getDecoder().decode(jigsawImageBase64);
|
|
|
+ byte[] backgroundBytes = Base64.getDecoder().decode(originalImageBase64);
|
|
|
+
|
|
|
+ // 3. 获取背景图宽度
|
|
|
+ BufferedImage backgroundImage = ImageIO.read(new ByteArrayInputStream(backgroundBytes));
|
|
|
+ if (backgroundImage == null) {
|
|
|
+ log.error("无法解码背景图");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int width = backgroundImage.getWidth();
|
|
|
+
|
|
|
+ // 4. 计算滑块X位置(对应Python的get_X)
|
|
|
+ int X = getX(targetBytes, backgroundBytes);
|
|
|
+
|
|
|
+ // 5. 计算归一化坐标(对应Python: t = 320 * t / setSize_imgWidth)
|
|
|
+ double t = 320.0 * X / width;
|
|
|
+ String pointJson = "{\"x\":" + t + ",\"y\":5}";
|
|
|
+
|
|
|
+ // 6. AES加密坐标(对应Python的aes_encrypt)
|
|
|
+ String encryptedPointJson = aesEncryptForSpilder(pointJson, secretKey);
|
|
|
+
|
|
|
+ // 7. 提交滑块验证(对应Python的check_spilder)
|
|
|
+ Map<String, Object> checkData = new HashMap<>();
|
|
|
+ checkData.put("captchaType", "blockPuzzle");
|
|
|
+ checkData.put("pointJson", encryptedPointJson);
|
|
|
+ checkData.put("token", token);
|
|
|
+
|
|
|
+ Map<String, Object> checkResult = requestComponent.checkSpilder(checkData, headers);
|
|
|
+
|
|
|
+ // 8. 判断初步验证结果
|
|
|
+ if (checkResult == null || !"0000".equals(checkResult.get("repCode"))) {
|
|
|
+ String repMsg = checkResult != null ? (String) checkResult.get("repMsg") : "滑块验证失败";
|
|
|
+ log.error("滑块初步验证失败: {}", repMsg);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("滑块初步验证通过,进行二次验证");
|
|
|
+
|
|
|
+ // 9. 二次验证(Python新增步骤)
|
|
|
+ // 对应Python: rstoken = check_spilder_res['repData']['token']; captchaVerification = aes_encrypt(rstoken + '---' + point_json, secretKey)
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> checkRepData = (Map<String, Object>) checkResult.get("repData");
|
|
|
+ if (checkRepData == null) {
|
|
|
+ log.error("滑块初步验证repData为空");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String rsToken = (String) checkRepData.get("token");
|
|
|
+ if (rsToken == null) {
|
|
|
+ log.error("滑块初步验证返回token为空");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String captchaVerification = aesEncryptForSpilder(rsToken + "---" + pointJson, secretKey);
|
|
|
+
|
|
|
+ Map<String, Object> verifyData = new HashMap<>();
|
|
|
+ verifyData.put("captchaVerification", captchaVerification);
|
|
|
+
|
|
|
+ Map<String, Object> verifyResult = requestComponent.verifySpilder(verifyData, validate0, validate1, headers);
|
|
|
+
|
|
|
+ // 10. 判断二次验证结果(对应Python: verify_spilder_res['repCode'] == '0000')
|
|
|
+ if (verifyResult != null && "0000".equals(verifyResult.get("repCode"))) {
|
|
|
+ log.info("滑块二次验证通过");
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ String repMsg = verifyResult != null ? (String) verifyResult.get("repMsg") : "滑块二次验证失败";
|
|
|
+ log.error("滑块二次验证失败: {}", repMsg);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("滑块验证异常: {}", e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算滑块匹配位置(对应Python的get_X方法,使用模板匹配算法)
|
|
|
+ *
|
|
|
+ * <p>通过比较拼图块与背景图的像素差异,找到拼图块在背景图中的最佳匹配位置</p>
|
|
|
+ *
|
|
|
+ * @param jigsawBytes 拼图块图片字节(带透明通道的滑块图)
|
|
|
+ * @param backgroundBytes 背景图图片字节
|
|
|
+ * @return 滑块X坐标
|
|
|
+ */
|
|
|
+ private int getX(byte[] jigsawBytes, byte[] backgroundBytes) {
|
|
|
+ try {
|
|
|
+ BufferedImage jigsaw = ImageIO.read(new ByteArrayInputStream(jigsawBytes));
|
|
|
+ BufferedImage background = ImageIO.read(new ByteArrayInputStream(backgroundBytes));
|
|
|
+
|
|
|
+ if (jigsaw == null || background == null) {
|
|
|
+ log.warn("无法解码滑块图片,使用默认X=0");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ int bgWidth = background.getWidth();
|
|
|
+ int bgHeight = background.getHeight();
|
|
|
+ int jgWidth = jigsaw.getWidth();
|
|
|
+ int jgHeight = jigsaw.getHeight();
|
|
|
+
|
|
|
+ // 搜索范围:只在X方向滑动(滑块验证通常只需要水平匹配)
|
|
|
+ int searchEnd = bgWidth - jgWidth;
|
|
|
+ if (searchEnd <= 0) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ double bestMatch = Double.MAX_VALUE;
|
|
|
+ int bestX = 0;
|
|
|
+
|
|
|
+ // 遍历X方向搜索最佳匹配位置
|
|
|
+ for (int x = 0; x <= searchEnd; x++) {
|
|
|
+ double diff = 0;
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ // 比较拼图块非透明像素与背景对应位置的像素差异
|
|
|
+ for (int jx = 0; jx < jgWidth; jx++) {
|
|
|
+ for (int jy = 0; jy < jgHeight; jy++) {
|
|
|
+ int jigsawPixel = jigsaw.getRGB(jx, jy);
|
|
|
+ int jigsawAlpha = (jigsawPixel >> 24) & 0xff;
|
|
|
+
|
|
|
+ // 只比较非透明的拼图像素
|
|
|
+ if (jigsawAlpha > 128) {
|
|
|
+ int bgPixel = background.getRGB(x + jx, jy);
|
|
|
+
|
|
|
+ int jr = (jigsawPixel >> 16) & 0xff;
|
|
|
+ int jg = (jigsawPixel >> 8) & 0xff;
|
|
|
+ int jb = jigsawPixel & 0xff;
|
|
|
+ int br = (bgPixel >> 16) & 0xff;
|
|
|
+ int bg = (bgPixel >> 8) & 0xff;
|
|
|
+ int bb = bgPixel & 0xff;
|
|
|
+
|
|
|
+ // 使用颜色差异的平方和作为相似度度量
|
|
|
+ diff += Math.pow(jr - br, 2) + Math.pow(jg - bg, 2) + Math.pow(jb - bb, 2);
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count > 0) {
|
|
|
+ diff /= count;
|
|
|
+ if (diff < bestMatch) {
|
|
|
+ bestMatch = diff;
|
|
|
+ bestX = x;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("滑块匹配结果: X={}, diff={}", bestX, bestMatch);
|
|
|
+ return bestX;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("滑块图片匹配异常: {}", e.getMessage(), e);
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AES加密(对应Python的aes_encrypt方法)
|
|
|
+ *
|
|
|
+ * <p>使用AES/ECB/PKCS5Padding模式加密,密钥不足16位时右侧补空格,超出16位截断</p>
|
|
|
+ *
|
|
|
+ * @param plainText 明文
|
|
|
+ * @param key 密钥
|
|
|
+ * @return Base64编码的密文
|
|
|
+ */
|
|
|
+ private String aesEncryptForSpilder(String plainText, String key) {
|
|
|
+ // 确保密钥长度为16字节(对应Python: key.ljust(16)[:16])
|
|
|
+ String paddedKey;
|
|
|
+ if (key.length() < 16) {
|
|
|
+ paddedKey = String.format("%-16s", key);
|
|
|
+ } else {
|
|
|
+ paddedKey = key.substring(0, 16);
|
|
|
+ }
|
|
|
+ return AESUtils.encryptECBPK5Base64(plainText, paddedKey);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 构建报价结果VO
|
|
|
*
|
|
|
@@ -543,10 +791,12 @@ public class RenBaoNewCrawlerRequestImpl implements RenBaoNewCrawlerRequest {
|
|
|
costs.setCiPremium(planFee);
|
|
|
totalPremium += fee;
|
|
|
} else if ("RM9".equals(payReason)) {
|
|
|
- vvTax = fee;
|
|
|
- costs.setVvTax(planFee);
|
|
|
+ vvTax = vvTax +fee;
|
|
|
+ totalPremium += fee;
|
|
|
+ } else if ("RM8".equals(payReason)) {
|
|
|
+ vvTax = vvTax +fee;
|
|
|
totalPremium += fee;
|
|
|
- } else if (proposalNo != null && proposalNo.contains("DAA")) {
|
|
|
+ }else if (proposalNo != null && proposalNo.contains("DAA")) {
|
|
|
biPremium += fee;
|
|
|
totalPremium += fee;
|
|
|
} else if (proposalNo != null && proposalNo.contains("DZA")) {
|