First Commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(name = "安卓 SDK Level 设置", initDefault = true, managed = false)
|
||||
public class AndroidSdkLevelConfig implements IConfig<AndroidSdkLevelConfig> {
|
||||
|
||||
Map<String, Integer> androidVerToSdk;
|
||||
|
||||
public AndroidSdkLevelConfig() {
|
||||
androidVerToSdk = new HashMap<>();
|
||||
androidVerToSdk.put("14", 34);
|
||||
androidVerToSdk.put("13", 33);
|
||||
androidVerToSdk.put("12L", 32);
|
||||
androidVerToSdk.put("12", 31);
|
||||
androidVerToSdk.put("11", 30);
|
||||
androidVerToSdk.put("10", 29);
|
||||
androidVerToSdk.put("9", 28);
|
||||
androidVerToSdk.put("8.1", 27);
|
||||
androidVerToSdk.put("8.0", 26);
|
||||
androidVerToSdk.put("7.1", 25);
|
||||
androidVerToSdk.put("7.0", 24);
|
||||
androidVerToSdk.put("6.0", 23);
|
||||
androidVerToSdk.put("5.1", 22);
|
||||
androidVerToSdk.put("5.0", 21);
|
||||
androidVerToSdk.put("4.4W", 20);
|
||||
androidVerToSdk.put("4.4", 19);
|
||||
androidVerToSdk.put("4.3", 18);
|
||||
androidVerToSdk.put("4.2", 17);
|
||||
androidVerToSdk.put("4.1", 16);
|
||||
androidVerToSdk.put("4.0.3", 15);
|
||||
androidVerToSdk.put("4.0.1", 14);
|
||||
androidVerToSdk.put("3.2", 13);
|
||||
androidVerToSdk.put("3.1", 12);
|
||||
androidVerToSdk.put("3.0", 11);
|
||||
androidVerToSdk.put("2.3.3", 10);
|
||||
androidVerToSdk.put("2.3", 9);
|
||||
androidVerToSdk.put("2.2", 8);
|
||||
androidVerToSdk.put("2.1", 7);
|
||||
androidVerToSdk.put("2.0.1", 6);
|
||||
androidVerToSdk.put("2.0", 5);
|
||||
androidVerToSdk.put("1.6", 4);
|
||||
androidVerToSdk.put("1.5", 3);
|
||||
androidVerToSdk.put("1.1", 2);
|
||||
androidVerToSdk.put("1.0", 1);
|
||||
}
|
||||
|
||||
public int getSdkLevel(String androidVersion) {
|
||||
if (androidVerToSdk.containsKey(androidVersion)) {
|
||||
return androidVerToSdk.get(androidVersion);
|
||||
}
|
||||
|
||||
// 模糊匹配前缀
|
||||
for (String key : androidVerToSdk.keySet()) {
|
||||
if (androidVersion.startsWith(key)) {
|
||||
return androidVerToSdk.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("未知的 Android 版本: " + androidVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.util.VersionComparator;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(name = "Chrome 版本设置", initDefault = true, managed = false)
|
||||
public class ChromeVersionsConfig implements IConfig<ChromeVersionsConfig> {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private List<String> chromeVersions;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private Integer chromeVersionsHashCode = null;
|
||||
|
||||
public ChromeVersionsConfig setChromeVersions(List<String> chromeVersions) {
|
||||
this.chromeVersions = chromeVersions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getChromeVersions() {
|
||||
int chromeVersionsHashCode = chromeVersions.hashCode();
|
||||
if (this.chromeVersionsHashCode == null || chromeVersionsHashCode != this.chromeVersionsHashCode) {
|
||||
Set<String> set = new HashSet<>(chromeVersions);
|
||||
chromeVersions = new ArrayList<>(set);
|
||||
chromeVersions.sort(VersionComparator.INSTANCE.reversed());
|
||||
this.chromeVersionsHashCode = chromeVersions.hashCode();
|
||||
}
|
||||
return chromeVersions;
|
||||
}
|
||||
|
||||
public ChromeVersionsConfig() {
|
||||
chromeVersions = new ArrayList<>(
|
||||
List.of("135.0.7049.100","135.0.7049.79","135.0.7049.38","134.0.6998.136","134.0.6998.135","134.0.6998.108","134.0.6998.95","133.0.6943.121","133.0.6943.50","133.0.6943.49","133.0.6943.39","132.0.6834.165","132.0.6834.163","132.0.6834.122","132.0.6834.79","131.0.6778.200","131.0.6778.136","131.0.6778.105","131.0.6778.81","131.0.6778.39","130.0.6723.103","130.0.6723.102","130.0.6723.86","130.0.6723.59","130.0.6723.58","130.0.6723.40","129.0.6668.100","129.0.6668.81","129.0.6668.71","128.0.6613.146","128.0.6613.127","128.0.6613.99","127.0.6533.106","127.0.6533.103","127.0.6533.84","127.0.6533.64","126.0.6478.186","126.0.6478.122","126.0.6478.72","126.0.6478.71","126.0.6478.50","125.0.6422.165","125.0.6422.164","125.0.6422.147","125.0.6422.146","125.0.6422.113","125.0.6422.72","125.0.6422.53","125.0.6422.52","124.0.6367.179","124.0.6367.172","124.0.6367.171","124.0.6367.114","124.0.6367.113","124.0.6367.83","124.0.6367.82","124.0.6367.54","123.0.6312.121","123.0.6312.120","123.0.6312.119","123.0.6312.118","123.0.6312.99","123.0.6312.80","123.0.6312.41","123.0.6312.40","122.0.6261.119","122.0.6261.106","122.0.6261.105","122.0.6261.91","122.0.6261.90","122.0.6261.64","122.0.6261.43","121.0.6167.178","121.0.6167.165","121.0.6167.164","121.0.6167.144","121.0.6167.143","121.0.6167.101","120.0.6099.230","120.0.6099.210","120.0.6099.194","120.0.6099.193","120.0.6099.145","120.0.6099.144","120.0.6099.116","120.0.6099.115","120.0.6099.44","120.0.6099.43","119.0.6045.194","119.0.6045.193","119.0.6045.164","119.0.6045.163","119.0.6045.134","119.0.6045.66","119.0.6045.53","118.0.5993.112","118.0.5993.111","118.0.5993.80","118.0.5993.65","118.0.5993.48","117.0.5938.154","117.0.5938.141","117.0.5938.140","117.0.5938.61","117.0.5938.60","116.0.5845.172","116.0.5845.164","116.0.5845.163","116.0.5845.114","116.0.5845.92","115.0.5790.136","114.0.5735.60","114.0.5735.53","113.0.5672.77","113.0.5672.76","112.0.5615.136","112.0.5615.101","112.0.5615.100","112.0.5615.48","111.0.5563.116","111.0.5563.115","111.0.5563.58","111.0.5563.49","110.0.5481.154","110.0.5481.153","110.0.5481.65","110.0.5481.64","110.0.5481.63","110.0.5481.61","109.0.5414.118","109.0.5414.117","109.0.5414.86","108.0.5359.128","108.0.5359.61","107.0.5304.141","107.0.5304.105","107.0.5304.91","106.0.5249.126","106.0.5249.79","106.0.5249.65","105.0.5195.136","105.0.5195.124","105.0.5195.79","105.0.5195.77","105.0.5195.68","104.0.5112.97","104.0.5112.69","103.0.5060.129","103.0.5060.71","103.0.5060.70","103.0.5060.53","102.0.5005.125","102.0.5005.99","102.0.5005.78","102.0.5005.59","101.0.4951.61","101.0.4951.41","100.0.4896.127","100.0.4896.88","100.0.4896.79","100.0.4896.58","99.0.4844.73","99.0.4844.58","99.0.4844.48","98.0.4758.101","98.0.4758.87","97.0.4692.98","97.0.4692.87","97.0.4692.70","96.0.4664.104","96.0.4664.92","95.0.4638.74","95.0.4638.50","94.0.4606.85","94.0.4606.80","94.0.4606.71","94.0.4606.61","94.0.4606.50","93.0.4577.82","93.0.4577.62","92.0.4515.166","92.0.4515.159","92.0.4515.131","92.0.4515.115","92.0.4515.105","91.0.4472.164","91.0.4472.134","91.0.4472.120","91.0.4472.114","91.0.4472.101","91.0.4472.88","91.0.4472.77","91.0.4472.16","90.0.4430.210","90.0.4430.91","90.0.4430.82","90.0.4430.66","89.0.4389.105","89.0.4389.90","89.0.4389.86","89.0.4389.72","88.0.4324.181","88.0.4324.155","88.0.4324.152","88.0.4324.141","88.0.4324.93","87.0.4280.141","87.0.4280.101","87.0.4280.86","87.0.4280.66","86.0.4240.198","86.0.4240.185","86.0.4240.114","86.0.4240.110","86.0.4240.99","86.0.4240.75","85.0.4183.127","85.0.4183.101","85.0.4183.81","84.0.4147.125","84.0.4147.105","84.0.4147.89","83.0.4103.106","83.0.4103.101","83.0.4103.96","83.0.4103.83","81.0.4044.138","81.0.4044.117","80.0.3987.162","80.0.3987.149","80.0.3987.132","80.0.3987.119","80.0.3987.117","80.0.3987.87","79.0.3945.136","79.0.3945.116","79.0.3945.79","78.0.3904.108","78.0.3904.96","78.0.3904.90","78.0.3904.62","77.0.3865.116","77.0.3865.92","77.0.3865.73","76.0.3809.132","76.0.3809.111","76.0.3809.89","75.0.3770.143","75.0.3770.101","75.0.3770.89","75.0.3770.67","74.0.3729.157","74.0.3729.149","74.0.3729.136","74.0.3729.112","73.0.3683.90","73.0.3683.75")
|
||||
);
|
||||
// 立即去重、排序、更新 hashCode
|
||||
getChromeVersions();
|
||||
}
|
||||
|
||||
public String getRandomChromeVersion() {
|
||||
int index = RANDOM.nextInt(chromeVersions.size());
|
||||
return getChromeVersions().get(index);
|
||||
}
|
||||
|
||||
public String getLatest() {
|
||||
return getChromeVersions().get(0);
|
||||
}
|
||||
|
||||
public String getOldest() {
|
||||
List<String> chromeVersions = getChromeVersions();
|
||||
return chromeVersions.get(chromeVersions.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ConfigInfo(
|
||||
field = "deviceInfo",
|
||||
name = "设备信息设置",
|
||||
initDefault = true,
|
||||
managed = false)
|
||||
public class DeviceInfoConfig implements IConfig<DeviceInfoConfig> {
|
||||
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
List<DeviceInfo> deviceInfos;
|
||||
|
||||
public DeviceInfoConfig() {
|
||||
deviceInfos = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机设备信息
|
||||
* @return
|
||||
*/
|
||||
public DeviceInfo getRandomDeviceInfo() {
|
||||
if (deviceInfos.isEmpty()) return null;
|
||||
return deviceInfos.get(RANDOM.nextInt(deviceInfos.size()));
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@Slf4j
|
||||
public static class DeviceInfo {
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String model;
|
||||
private String brand;
|
||||
private String product;
|
||||
private String device;
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String deviceType;
|
||||
private String versionRelease;
|
||||
private String buildId;
|
||||
private String buildNumber;
|
||||
private String buildType;
|
||||
private String buildTags;
|
||||
|
||||
public static final Pattern PATTERN = Pattern.compile("^(?<brand>.*?)/(?<product>.*?)/(?<device>.*?):(?<versionRelease>.*?)/(?<buildId>.*?)/(?<buildNumber>.*?):(?<buildType>.*?)/(?<buildTags>.*?)$");
|
||||
|
||||
|
||||
private DeviceInfo() {
|
||||
}
|
||||
|
||||
public DeviceInfo setFingerprint(String fingerprint) {
|
||||
Matcher m = PATTERN.matcher(fingerprint);
|
||||
if (!m.matches()) {
|
||||
throw new IllegalArgumentException("Fingerprint not match the pattern: " + fingerprint);
|
||||
}
|
||||
this.setBrand(m.group("brand"));
|
||||
this.setBuildId(m.group("buildId"));
|
||||
this.setBuildNumber(m.group("buildNumber"));
|
||||
this.setBuildTags(m.group("buildTags"));
|
||||
this.setBuildType(m.group("buildType"));
|
||||
this.setProduct(m.group("product"));
|
||||
this.setVersionRelease(m.group("versionRelease"));
|
||||
this.setDevice(m.group("device"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DeviceInfo setDeviceType(String deviceType) {
|
||||
if (!"Mobile".equals(deviceType) && !"Pad".equals(deviceType)) {
|
||||
throw new IllegalArgumentException("DeviceType must be \"Mobile\" or \"Pad\", but got \"" + deviceType + "\"");
|
||||
}
|
||||
this.deviceType = deviceType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定 model、fingerprint 和 deviceType 返回 DeviceInfo
|
||||
* @param model
|
||||
* @param fingerprint
|
||||
* @param deviceType
|
||||
* @return
|
||||
*/
|
||||
public static DeviceInfo from(String model, String fingerprint, String deviceType) {
|
||||
|
||||
DeviceInfo deviceInfo = new DeviceInfo();
|
||||
deviceInfo.setModel(model);
|
||||
deviceInfo.setFingerprint(fingerprint);
|
||||
deviceInfo.setDeviceType(deviceType);
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
public static DeviceInfo from(String model, String fingerprint) {
|
||||
return from(model, fingerprint, "Mobile");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从设备信息中还原 fingerprint
|
||||
* @return
|
||||
*/
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
public String getFingerprint() {
|
||||
return String.format("%s/%s/%s:%s/%s/%s:%s/%s",
|
||||
getBrand(), getProduct(), getDevice(), getVersionRelease(),
|
||||
getBuildId(), getBuildNumber(), getBuildType(), getBuildTags()
|
||||
);
|
||||
}
|
||||
|
||||
public final String toString() {
|
||||
return String.format("Model: %s, Fingerprint: %s",
|
||||
getModel(), getFingerprint()
|
||||
);
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
DeviceInfo other = (DeviceInfo)obj;
|
||||
return hashCode() == other.hashCode();
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(getModel(), getFingerprint());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
import quant.rich.emoney.interfaces.ValidEmoneyRequestConfig;
|
||||
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
|
||||
import quant.rich.emoney.util.EncryptUtils;
|
||||
import quant.rich.emoney.util.SpringContextHolder;
|
||||
import quant.rich.emoney.util.TextUtils;
|
||||
|
||||
/**
|
||||
* 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
|
||||
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
@ValidEmoneyRequestConfig
|
||||
@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
|
||||
public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
|
||||
|
||||
/**
|
||||
* 代理类型
|
||||
*/
|
||||
private Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
/**
|
||||
* 代理主机
|
||||
*/
|
||||
private String proxyHost;
|
||||
|
||||
/**
|
||||
* 代理端口
|
||||
*/
|
||||
private Integer proxyPort;
|
||||
|
||||
/**
|
||||
* 是否忽略 HTTPS 证书校验
|
||||
*/
|
||||
private Boolean ignoreHttpsVerification = false;
|
||||
|
||||
/**
|
||||
* 是否匿名登录
|
||||
*/
|
||||
private Boolean isAnonymous = true;
|
||||
|
||||
/**
|
||||
* 非匿名登录时的用户名
|
||||
*/
|
||||
private String username = "";
|
||||
|
||||
/**
|
||||
* 非匿名登录时的密码
|
||||
*/
|
||||
private String password = "";
|
||||
|
||||
/**
|
||||
* 鉴权信息
|
||||
*/
|
||||
private String authorization;
|
||||
|
||||
/**
|
||||
* 当前 authorization 更新时间
|
||||
*/
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime authorizationUpdateTime;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><br>本例随机生成并管理,需要符合 16 位
|
||||
*
|
||||
*/
|
||||
private String androidId = TextUtils.randomString("abcdef0123456789", 16);
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>Webview <code><i>User-Agent</i></li>
|
||||
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
|
||||
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
* @see AndroidSdkLevelConfig
|
||||
*/
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private String androidSdkLevel;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String softwareType;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
|
||||
* </ul>
|
||||
* 一般由程序所使用的 OkHttp 版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String okHttpUserAgent = "okhttp/3.12.2";
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.MODEL, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.FINGERPRINT, <b>用于:</b><ul>
|
||||
* <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
|
||||
* <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
|
||||
* </ul>
|
||||
* <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String fingerprint;
|
||||
|
||||
|
||||
/**
|
||||
* 对应 build.prop 中 Build.ID, <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
|
||||
* @see DeviceInfoConfig
|
||||
*
|
||||
*/
|
||||
private String buildId;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>WebView <code><i>User-Agent</i></code></li>
|
||||
* </ul>
|
||||
* <b>来源:</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
|
||||
* @see ChromeVersionsConfig
|
||||
*/
|
||||
private String chromeVersion;
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
|
||||
* EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
|
||||
* </ul>
|
||||
* 由程序版本决定<br>
|
||||
* <b>来源:</b>本例管理
|
||||
* @see EmoneyRequestConfig.androidSdkLevel
|
||||
*/
|
||||
private String emoneyVersion = "5.8.1";
|
||||
|
||||
/**
|
||||
* <b>用于:</b><ul>
|
||||
* <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
|
||||
* </ul>
|
||||
* 由程序决定, 一般默认为 "1"<br>
|
||||
* <b>来源:</b>本例管理
|
||||
*/
|
||||
private String emappViewMode = "1";
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private AndroidSdkLevelConfig androidSdkLevelConfig;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private DeviceInfoConfig deviceInfoConfig;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Autowired
|
||||
private ChromeVersionsConfig chromeVersionsConfig;
|
||||
|
||||
public void afterBeanInit() {
|
||||
|
||||
try {
|
||||
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
|
||||
deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class));
|
||||
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
log.debug("SpringContext not ready");
|
||||
}
|
||||
|
||||
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
|
||||
// 任意是 null 的都要统一由 deviceInfo 进行设置
|
||||
initFromRandomDeviceInfo();
|
||||
}
|
||||
else {
|
||||
// 都不是 null,则由 fingerprint 来检查各项
|
||||
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
|
||||
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
|
||||
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
|
||||
// 于本地,万一本地的 deviceInfo.(fallback.)json 也不值得信任?
|
||||
// 所以只检查 fingerprint
|
||||
|
||||
DeviceInfo deviceInfo;
|
||||
boolean valid = true;
|
||||
try {
|
||||
deviceInfo = DeviceInfo.from(null, fingerprint);
|
||||
Validate.validState(androidVersion.equals(
|
||||
deviceInfo.getVersionRelease()),
|
||||
"androidVersion(versionRelease) doesn't match");
|
||||
Validate.validState(androidSdkLevel.equals(
|
||||
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
|
||||
"androidSdkLevel doesn't match");
|
||||
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
|
||||
"buildId doesn't match");
|
||||
}
|
||||
catch (Exception e) {
|
||||
valid = false;
|
||||
}
|
||||
if (!valid) {
|
||||
initFromRandomDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeVersion == null) {
|
||||
chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
|
||||
}
|
||||
|
||||
// 注入 OkHttp
|
||||
PatchOkHttp.apply(
|
||||
r -> r
|
||||
.hostEndsWith("emoney.cn")
|
||||
.or(a -> a.hostContains("emapp"))
|
||||
.or(b -> b.hasHeaderName("X-Protocol-Id"))
|
||||
.overrideIf("User-Agent", getOkHttpUserAgent()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从随机 deviceInfo 填充本例相关字段
|
||||
* @return
|
||||
*/
|
||||
private EmoneyRequestConfig initFromRandomDeviceInfo() {
|
||||
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
|
||||
// 更新 deviceInfo 后对应 androidId 也要修改,哪怕原来非空
|
||||
androidId = TextUtils.randomString("abcdef0123456789", 16);
|
||||
return initFromDeviceInfo(deviceInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定 deviceInfo 填充本例相关字段
|
||||
* @param deviceInfo
|
||||
* @return
|
||||
*/
|
||||
private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) {
|
||||
if (deviceInfo == null) {
|
||||
log.error("deviceInfo is null");
|
||||
RuntimeException e = new RuntimeException("deviceInfo is null");
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
deviceName = deviceInfo.getModel();
|
||||
androidVersion = deviceInfo.getVersionRelease();
|
||||
androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
|
||||
softwareType = deviceInfo.getDeviceType();
|
||||
fingerprint = deviceInfo.getFingerprint();
|
||||
buildId = deviceInfo.getBuildId();
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig() {}
|
||||
|
||||
public EmoneyRequestConfig setFingerprint(String fingerprint) {
|
||||
// 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下
|
||||
// 因为为 null 时不一定是程序初始化时,也有可能是从前端 Post 而来的
|
||||
try {
|
||||
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
log.debug("SpringContext not ready");
|
||||
}
|
||||
|
||||
if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
|
||||
DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
|
||||
initFromDeviceInfo(deviceInfo);
|
||||
}
|
||||
else {
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前配置获取 guid,用于益盟登录接口
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getGuid() {
|
||||
return EncryptUtils.toMD5String(androidId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getXAndroidAgent() {
|
||||
// EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
|
||||
return
|
||||
new StringBuilder()
|
||||
.append("EMAPP/")
|
||||
.append(getEmoneyVersion())
|
||||
.append("(Android;")
|
||||
.append(getAndroidSdkLevel())
|
||||
.append(")").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于 App 内用到 Webview 的地方
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getWebviewUserAgent() {
|
||||
return new StringBuilder()
|
||||
.append("Mozilla/5.0 (Linux; Android ")
|
||||
.append(getAndroidVersion())
|
||||
.append("; ")
|
||||
.append(getDeviceName())
|
||||
.append(" Build/")
|
||||
.append(getBuildId())
|
||||
.append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
|
||||
.append(getChromeVersion())
|
||||
.append(" Mobile Safari/537.36")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于 App 内少量未用到 Webview 的地方,如首页获取图片等
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getNonWebviewResourceUserAgent() {
|
||||
// Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
|
||||
return new StringBuilder()
|
||||
.append("Dalvik/2.1.0 (Linux; U; Android ")
|
||||
.append(getAndroidVersion())
|
||||
.append(";")
|
||||
.append(getDeviceName())
|
||||
.append(" Build/")
|
||||
.append(getBuildId())
|
||||
.append(")")
|
||||
.toString();
|
||||
}
|
||||
/**
|
||||
* 根据当前配置获取 hardware,用于益盟登录接口
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHardware() {
|
||||
return EncryptUtils.toMD5String(getFingerprint());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode
|
||||
* @return
|
||||
*/
|
||||
public ObjectNode getUsernamePasswordLoginObject() {
|
||||
return getUsernamePasswordLoginObject(username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
|
||||
* @param username 用户名
|
||||
* @param password 密码(可以是加密过的,也可以是明文)
|
||||
* @return
|
||||
*/
|
||||
public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
|
||||
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
|
||||
}
|
||||
|
||||
ObjectNode node = getAnonymousLoginObject();
|
||||
node.put("accId", username);
|
||||
node.put("accType", 1);
|
||||
|
||||
// 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的
|
||||
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
|
||||
|
||||
node.put("pwd",
|
||||
tryDecryptPassword != null ? password :
|
||||
EncryptUtils.encryptAesForEmoneyPassword(password)
|
||||
);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本例信息生成一个用于匿名登录的 ObjectNode
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public ObjectNode getAnonymousLoginObject() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
ObjectNode exIdentify = mapper.createObjectNode();
|
||||
exIdentify.put("IMEI", "");
|
||||
exIdentify.put("AndroidID", getAndroidId());
|
||||
exIdentify.put("MAC", "");
|
||||
exIdentify.put("OSFingerPrint", getFingerprint());
|
||||
String exIdentifyString = exIdentify.toString().replace("/", "\\/");
|
||||
|
||||
// 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
|
||||
String guid = getGuid();
|
||||
node.put("appVersion", getEmoneyVersion());
|
||||
node.put("productId", 4);
|
||||
node.put("softwareType", getSoftwareType());
|
||||
node.put("deviceName", getDeviceName());
|
||||
node.put("ssid", "0");
|
||||
node.put("platform", "android");
|
||||
node.put("exIdentify", exIdentifyString);
|
||||
node.put("osVersion", getAndroidSdkLevel());
|
||||
node.put("accId", guid);
|
||||
node.put("guid", guid);
|
||||
node.put("accType", 4);
|
||||
node.put("pwd", "");
|
||||
node.put("channelId", "1711");
|
||||
node.put("hardware", getHardware());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码:<ul>
|
||||
* <li>null or empty,保存空字符串</li>
|
||||
* <li>尝试解密成功,说明是密文,直接保存</li>
|
||||
* <li>尝试解密失败,说明是明文,加密保存</li>
|
||||
* </ul>
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public EmoneyRequestConfig setPassword(String password) {
|
||||
if (StringUtils.isEmpty(password)) {
|
||||
this.password = "";
|
||||
return this;
|
||||
}
|
||||
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
|
||||
if (tryDecryptPassword != null) {
|
||||
this.password = password;
|
||||
}
|
||||
else {
|
||||
this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 androidVersion/androidSdkLevel 不为 null
|
||||
*/
|
||||
public EmoneyRequestConfig beforeSaving() {
|
||||
setFingerprint(this.fingerprint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发更新 Authorization 的更新时间为现在
|
||||
* @return
|
||||
*/
|
||||
public EmoneyRequestConfig updateAuthorizationTime() {
|
||||
this.authorizationUpdateTime = LocalDateTime.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig mergeTo(EmoneyRequestConfig other) {
|
||||
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
|
||||
IConfig.super.mergeTo(other);
|
||||
if (authorizationUpdated) {
|
||||
other.updateAuthorizationTime();
|
||||
}
|
||||
return other;
|
||||
}
|
||||
|
||||
public EmoneyRequestConfig mergeFrom(EmoneyRequestConfig other) {
|
||||
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
|
||||
IConfig.super.mergeFrom(other);
|
||||
if (authorizationUpdated) {
|
||||
this.updateAuthorizationTime();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
Normal file
38
src/main/java/quant/rich/emoney/entity/config/IndexInfo.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class IndexInfo {
|
||||
|
||||
private List<ParamInfo> paramInfoList = new ArrayList<>();
|
||||
|
||||
private String code;
|
||||
|
||||
private String name;
|
||||
|
||||
private Boolean isCalc;
|
||||
|
||||
private List<StockSpan> supportPeriod = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public static class ParamInfo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer max;
|
||||
|
||||
private Integer min;
|
||||
|
||||
private Integer defaultValue;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jodd.io.FileUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.GzipSource;
|
||||
import okio.Okio;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
/**
|
||||
* 指标信息配置,只做运行时管理,不做保存
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@Lazy(false)
|
||||
@ConfigInfo(field = "indexInfo", name =" 指标信息配置", initDefault = true, managed = false)
|
||||
public class IndexInfoConfig implements IConfig<IndexInfoConfig> {
|
||||
|
||||
private static final String CONFIG_IND_ONLINE_PATH = "./conf/extra/config_ind_online.json";
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private String configIndOnlineUrl;
|
||||
|
||||
@JsonView(IConfig.Views.Persistence.class)
|
||||
private JsonNode configIndOnline;
|
||||
|
||||
@Autowired
|
||||
@JsonIgnore
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private EmoneyRequestConfig emoneyRequestConfig;
|
||||
|
||||
public IndexInfoConfig() {
|
||||
try {
|
||||
String configStr = FileUtil.readString(CONFIG_IND_ONLINE_PATH);
|
||||
configIndOnline = new ObjectMapper().readTree(configStr);
|
||||
}
|
||||
catch (Exception e) {}
|
||||
configIndOnlineUrl = "https://emapp-static.oss-cn-shanghai.aliyuncs.com/down13/emstock/config/ind_config/v1000003/config_ind_online.json";
|
||||
}
|
||||
|
||||
public String getConfigIndOnlineStr() {
|
||||
return getConfigIndOnline().toPrettyString();
|
||||
}
|
||||
|
||||
public String getOnlineConfigByUrl(String url) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", emoneyRequestConfig.getOkHttpUserAgent())
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.header("Connection", "Keep-Alive")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.get()
|
||||
// Host 不能在 OkHttp 中直接设置(由 URL 控制)
|
||||
.build();
|
||||
|
||||
// 发出请求
|
||||
Response backendResponse = getInstance().newCall(request).execute();
|
||||
if (backendResponse.body() != null) {
|
||||
// 将内容复制给前端
|
||||
return
|
||||
new String(backendResponse.body().bytes(), "UTF-8");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static OkHttpClient okHttpClient;
|
||||
|
||||
public static ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
|
||||
|
||||
public static OkHttpClient getInstance() {
|
||||
if (okHttpClient == null) { //加同步安全
|
||||
synchronized (OkHttpClient.class) {
|
||||
if (okHttpClient == null) { //okhttp可以缓存数据....指定缓存路径
|
||||
okHttpClient = new OkHttpClient.Builder()//构建器
|
||||
.proxy(Proxy.NO_PROXY) //来屏蔽系统代理
|
||||
.connectionPool(connectionPool)
|
||||
.sslSocketFactory(getSSLSocketFactory(), getX509TrustManager())
|
||||
.hostnameVerifier(getHostnameVerifier())
|
||||
.connectTimeout(600, TimeUnit.SECONDS)//连接超时
|
||||
.writeTimeout(600, TimeUnit.SECONDS)//写入超时
|
||||
.readTimeout(600, TimeUnit.SECONDS)//读取超时
|
||||
.addNetworkInterceptor(new GzipResponseInterceptor())
|
||||
.build();
|
||||
okHttpClient.dispatcher().setMaxRequestsPerHost(200);
|
||||
okHttpClient.dispatcher().setMaxRequests(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
return okHttpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* description 忽略https证书验证
|
||||
*/
|
||||
private static HostnameVerifier getHostnameVerifier() {
|
||||
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String s, SSLSession sslSession) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
return hostnameVerifier;
|
||||
}
|
||||
/**
|
||||
* description 忽略https证书验证
|
||||
*/
|
||||
private static SSLSocketFactory getSSLSocketFactory() {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, getTrustManager(), new SecureRandom());
|
||||
return sslContext.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509TrustManager getX509TrustManager() {
|
||||
X509TrustManager trustManager = null;
|
||||
try {
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
|
||||
}
|
||||
trustManager = (X509TrustManager) trustManagers[0];
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return trustManager;
|
||||
}
|
||||
|
||||
private static TrustManager[] getTrustManager() {
|
||||
TrustManager[] trustAllCerts = 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[]{};
|
||||
}
|
||||
}
|
||||
};
|
||||
return trustAllCerts;
|
||||
}
|
||||
|
||||
public static class GzipResponseInterceptor implements Interceptor {
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Response response = chain.proceed(request);
|
||||
|
||||
// 只有服务器返回了 gzip 编码才处理
|
||||
if ("gzip".equalsIgnoreCase(response.header("Content-Encoding"))) {
|
||||
// 原始响应体
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) return response;
|
||||
|
||||
// 用 GzipSource 包装原始流,并缓冲
|
||||
GzipSource gzippedResponseBody = new GzipSource(body.source());
|
||||
BufferedSource unzippedSource = Okio.buffer(gzippedResponseBody);
|
||||
|
||||
// 构造一个新的 ResponseBody,不再带 Content-Encoding/Length
|
||||
ResponseBody newBody = ResponseBody.create(
|
||||
unzippedSource,
|
||||
body.contentType(),
|
||||
-1L
|
||||
);
|
||||
|
||||
// 去掉 Content-Encoding/Length,让后续调用 body().string() 时拿到解压后的内容
|
||||
return response.newBuilder()
|
||||
.removeHeader("Content-Encoding")
|
||||
.removeHeader("Content-Length")
|
||||
.body(newBody)
|
||||
.build();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.interfaces.ConfigInfo;
|
||||
import quant.rich.emoney.interfaces.IConfig;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ConfigInfo(field = "platform", name = "平台设置", initDefault = true)
|
||||
public class PlatformConfig implements IConfig<PlatformConfig> {
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private Boolean isInited;
|
||||
|
||||
public PlatformConfig() {
|
||||
username = "admin";
|
||||
password = "";
|
||||
isInited = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package quant.rich.emoney.entity.config;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class SmartViewWriter {
|
||||
private static final ObjectMapper normalMapper = new ObjectMapper();
|
||||
private static final ObjectMapper withViewMapper = JsonMapper.builder()
|
||||
.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false)
|
||||
.build();
|
||||
|
||||
public SmartViewWriter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能序列化:如果对象包含 @JsonView 注解字段,则只输出视图字段;否则输出全部字段
|
||||
*/
|
||||
public String writeWithSmartView(Object obj, Class<?> viewClass) {
|
||||
if (hasJsonViewRecursive(obj.getClass(), new HashSet<>())) {
|
||||
try {
|
||||
ObjectWriter writer = withViewMapper.writerWithView(viewClass);
|
||||
writer = writer.with(SerializationFeature.INDENT_OUTPUT);
|
||||
return writer.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Serialization failed", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
ObjectWriter writer = normalMapper.writer();
|
||||
writer = writer.with(SerializationFeature.INDENT_OUTPUT);
|
||||
return writer.writeValueAsString(obj); // 无视图注解,走默认序列化
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Serialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasJsonViewRecursive(Class<?> clazz, Set<Class<?>> visited) {
|
||||
if (clazz == null || clazz.getName().startsWith("java.") || visited.contains(clazz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.add(clazz);
|
||||
|
||||
JavaType javaType = normalMapper.getTypeFactory().constructType(clazz);
|
||||
BeanDescription beanDesc = normalMapper.getSerializationConfig().introspect(javaType);
|
||||
|
||||
for (BeanPropertyDefinition prop : beanDesc.findProperties()) {
|
||||
AnnotatedMember accessor = prop.getAccessor();
|
||||
if (accessor != null && accessor.hasAnnotation(JsonView.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
JavaType fieldType = prop.getPrimaryType();
|
||||
|
||||
if (fieldType == null) continue;
|
||||
|
||||
if (fieldType.isCollectionLikeType()) {
|
||||
// 处理 List<T>, Set<T>
|
||||
JavaType contentType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(contentType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (fieldType.isArrayType()) {
|
||||
JavaType elemType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(elemType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (fieldType.isMapLikeType()) {
|
||||
JavaType valueType = fieldType.getContentType();
|
||||
if (hasJsonViewRecursive(valueType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
} else if (!fieldType.isPrimitive() && !fieldType.isEnumType()) {
|
||||
// 普通对象类型递归
|
||||
if (hasJsonViewRecursive(fieldType.getRawClass(), visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
110
src/main/java/quant/rich/emoney/entity/postgre/EmoneyIndex.java
Normal file
110
src/main/java/quant/rich/emoney/entity/postgre/EmoneyIndex.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package quant.rich.emoney.entity.postgre;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.enums.StockSpan;
|
||||
|
||||
/**
|
||||
* 益盟指标
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class EmoneyIndex {
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String tsCode;
|
||||
/**
|
||||
* 时间
|
||||
*/
|
||||
@TableField("trade_date")
|
||||
private Date date;
|
||||
/**
|
||||
* 指标值
|
||||
*/
|
||||
private Long value;
|
||||
/**
|
||||
* 指标名称
|
||||
*/
|
||||
private String indexName;
|
||||
/**
|
||||
* 指标线名称
|
||||
*/
|
||||
private String lineName;
|
||||
/**
|
||||
* 指标参数
|
||||
*/
|
||||
private JsonNode indexParam;
|
||||
/**
|
||||
* 指标形状
|
||||
*/
|
||||
private Integer lineShape;
|
||||
/**
|
||||
* 数据粒度
|
||||
*/
|
||||
private Integer dataPeriod;
|
||||
|
||||
public StockSpan getStockSpan() {
|
||||
return StockSpan.fromEmoneyCode(dataPeriod);
|
||||
}
|
||||
|
||||
public EmoneyIndex setStockSpan(StockSpan stockSpan) {
|
||||
|
||||
if (dataPeriod == null) {
|
||||
dataPeriod = stockSpan.getEmoneyCode();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmoneyIndex setIndexParam(@SuppressWarnings("rawtypes") Map map) {
|
||||
if (map == null) {
|
||||
indexParam = mapper.createObjectNode();
|
||||
}
|
||||
else {
|
||||
indexParam = mapper.valueToTree(map);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonNode getIndexParam() {
|
||||
if (indexParam == null) {
|
||||
indexParam = mapper.createObjectNode();
|
||||
}
|
||||
return indexParam;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return mapper.valueToTree(this).toString();
|
||||
}
|
||||
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) return true;
|
||||
if (other == null || getClass() != other.getClass()) return false;
|
||||
EmoneyIndex index = (EmoneyIndex) other;
|
||||
return this.toString().equals(index.toString());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(tsCode, date, value, indexName, lineName, indexParam, lineShape, dataPeriod);
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/java/quant/rich/emoney/entity/sqlite/Plan.java
Normal file
67
src/main/java/quant/rich/emoney/entity/sqlite/Plan.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package quant.rich.emoney.entity.sqlite;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import quant.rich.emoney.mybatis.typehandler.CommaListTypeHandler;
|
||||
import quant.rich.emoney.mybatis.typehandler.JsonStringTypeHandler;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "plan", autoResultMap = true)
|
||||
public class Plan {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private String planId;
|
||||
|
||||
private String cronExpression;
|
||||
|
||||
private String planName;
|
||||
|
||||
private String indexCode;
|
||||
|
||||
@TableField(typeHandler = CommaListTypeHandler.class)
|
||||
private List<String> periods;
|
||||
|
||||
@TableField(typeHandler = JsonStringTypeHandler.class)
|
||||
private JsonNode params;
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
private Boolean openDayCheck;
|
||||
|
||||
public Plan setPeriods(List<String> periods) {
|
||||
if (CollectionUtils.isEmpty(periods)) {
|
||||
this.periods = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
Set<String> hashSet = new HashSet<>();
|
||||
periods.forEach(s -> {
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
hashSet.add(s.trim());
|
||||
}
|
||||
});
|
||||
this.periods = new ArrayList<>(hashSet);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getPeriods() {
|
||||
setPeriods(periods);
|
||||
return periods;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package quant.rich.emoney.entity.sqlite;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "protocol_match", autoResultMap = true)
|
||||
public class ProtocolMatch {
|
||||
|
||||
@TableId
|
||||
private Integer protocolId;
|
||||
|
||||
private String className;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user