Commit fbd8d21a authored by shanglipeng's avatar shanglipeng

init

parent 274ca314
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.greatchn</groupId>
<artifactId>testspider</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.8.0</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/net.lightbody.bmp/browsermob-core -->
<dependency>
<groupId>net.lightbody.bmp</groupId>
<artifactId>browsermob-core</artifactId>
<version>2.1.5</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-annotations</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-core</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-databind</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-parameter-names -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>13.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.greatchn;
import com.greatchn.yzh.TaskExecutor;
/**
* RPA With Selenium for Java 入口
*
* @author LWang 2023.01.28
* @since 1.0.0
*/
public class Main {
public static void main(String[] args) {
new TaskExecutor();
}
}
\ No newline at end of file
package com.greatchn.etax;
import cn.hutool.core.lang.ClassScanner;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.rpa.RpaCore;
import com.greatchn.rpa.config.RpaConfig;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Objects;
/**
* 电子税务局业务对象基础类
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
public abstract class BaseEtax {
private final EtaxConfig config;
private final RpaCore rpa;
private final LinkedHashMap<Class<? extends BaseFunction>, BaseFunction> functions;
protected static final String[] ROLES_TAX = {"法人", "法定代表人", "财务负责人", "办税员"};
protected static final String[] ROLES_INVOICE = {"法人", "法定代表人", "财务负责人", "开票员"};
/**
* 停用服务
*/
public void shutdown() {
if (Objects.isNull(rpa)) {
return;
}
rpa.stop();
}
/**
* 构造电子税务局业务对象
*
* @param config RPA 配置
* @param etaxConfigFile 电子税务局配置
*/
protected BaseEtax(RpaConfig config, String etaxConfigFile) {
this.config = EtaxConfig.build(etaxConfigFile);
this.rpa = new RpaCore(config);
this.functions = loadFunctions(this.getClass().getPackage());
}
/**
* 在指定包下,扫描 BaseFunction 的子类,并进行初始化
*
* @param pg 要扫描的包
* @return 扫描结果
*/
public LinkedHashMap<Class<? extends BaseFunction>, BaseFunction> loadFunctions(Package pg) {
final var result = new LinkedHashMap<Class<? extends BaseFunction>, BaseFunction>();
ClassScanner.scanPackage(pg.getName(), BaseFunction.class::isAssignableFrom).forEach(c -> {
try {
if (!Modifier.isAbstract(c.getModifiers())) {
result.put(
(Class<BaseFunction>) c,
(BaseFunction) c.getConstructor(BaseEtax.class).newInstance(this)
);
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
});
return result;
}
protected EtaxConfig config() {
return this.config;
}
protected RpaCore rpa() {
return this.rpa;
}
protected LinkedHashMap<Class<? extends BaseFunction>, BaseFunction> functions() {
return this.functions;
}
public <T extends BaseFunction> T function(Class<T> functionClass) {
var function = this.functions.get(functionClass);
if (functionClass.isInstance(function)) {
return (T) this.functions.get(functionClass);
} else {
throw new EtaxException(functionClass.getName() + " 方法对象不存在!");
}
}
private String[] roles = ROLES_TAX;
/**
* 角色类型:办税角色
*/
public final static int ROLE_TYPE_TAX = 0;
/**
* 角色类型:发票角色
*/
public final static int ROLE_TYPE_INVOICE = 1;
/**
* 设置角色
*
* @param roleType 角色类型:0:办税;1:发票
*/
protected void setRole(int roleType) {
this.roles = ROLE_TYPE_TAX == roleType ? ROLES_TAX : ROLES_INVOICE;
}
/**
* 获取当前任务所需的角色列表
*
* @return 角色列表
*/
public String[] getRoles() {
return this.roles;
}
}
package com.greatchn.etax;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.etax.exceptions.RetryWarning;
import com.greatchn.etax.sms.exceptions.SmsException;
import com.greatchn.kits.DateTimeKits;
import com.greatchn.rpa.RpaCore;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.NoSuchElementException;
import java.time.temporal.ChronoUnit;
/**
* 方法基础类
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public abstract class BaseFunction {
private final BaseEtax etax;
private final EtaxConfig config;
private final RpaCore rpa;
protected static final int SLEEP_TIME = 3;
protected static final long WAIT_TIME = 10;
protected static final long MOUSE_OFFSET = 50;
protected BaseEtax etax() {
return this.etax;
}
protected EtaxConfig config() {
return this.config;
}
protected RpaCore rpa() {
return this.rpa;
}
public BaseFunction(BaseEtax etax) {
this.etax = etax;
this.config = etax.config();
this.rpa = etax.rpa();
}
/**
* 统一异常处理
*
* @param cause 要处理的异常
* @param messagePrefix 消息前缀
* @return 异常消息描述
*/
protected <T> FunctionResult<T> handleException(Throwable cause, String messagePrefix) {
// 1. 处理需要单独处理的异常
if (cause instanceof NoSuchElementException e) {
return new FunctionResult<>(false, messagePrefix + e.getMessage(), null);
} else if (cause instanceof EtaxException e) {
return new FunctionResult<>(false, messagePrefix + e.getMessage(), null);
} else if (cause instanceof SmsException e) {
switch (e.getFlag()) {
case SmsException.PHONE_LOCK -> throw new RetryWarning(5, e.getMessage());
case SmsException.PHONE_LIMIT_RANGE -> throw new RetryWarning(15, e.getMessage());
case SmsException.PHONE_FULL_DAY ->
throw new RetryWarning(Math.abs(DateTimeKits.timeRemaining(ChronoUnit.MINUTES)), e.getMessage());
}
} else if (cause instanceof RetryWarning e) {
throw e;
}
// 2. 其他异常使用 messagePrefix 包裹后,返回失败执行结果
log.error("发生未知的异常,", cause);
return new FunctionResult<>(false, messagePrefix + "发生未知异常!", null);
}
}
package com.greatchn.etax;
import com.greatchn.kits.YamlKits;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* 电子税务局访问配置文件
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@ToString
@NoArgsConstructor
public class EtaxConfig {
/**
* 电子税务局域名(URL,完整域名,如:<a href="https://www.example.com/">https://www.example.com</a>)
*/
@Setter
@Getter
private String domain;
/**
* 企业电子税务局主页(URL,含完整域名,如:<a href="https://www.example.com/index.html">https://www.example.com/index.html</a>)
*/
@Setter
@Getter
private String enterpriseMainUrl;
/**
* 自然人电子税务局主页(URL,含完整域名,如:<a href="">https://www.example.com/index.html</a>)
*/
@Setter
@Getter
private String humanMainUrl;
/**
* 企业电子税务局主页(URI,不含完整域名,如:/index.html)
*/
@Setter
@Getter
private String enterpriseHomeUri;
/**
* 自然人电子税务局主页(URI,不含完整域名,如:/index.html)
*/
@Setter
@Getter
private String humanHomeUri;
public String getMainUrl() {
return LOGIN_TYPE_ENTERPRISE == loginType ? enterpriseMainUrl : humanMainUrl;
}
public String getHomeUri() {
return LOGIN_TYPE_ENTERPRISE == loginType ? enterpriseHomeUri : humanHomeUri;
}
public static final int LOGIN_TYPE_ENTERPRISE = 0;
public static final int LOGIN_TYPE_HUMAN = 1;
/**
* 登录用户类型:0,企业;1,自然人
*/
@Getter
@Setter
private int loginType = 0;
/**
* 创建电子税务局访问配置文件
*
* @param config 配置文件
* @return 配置对象
*/
public static EtaxConfig build(String config) {
return YamlKits.load(config, EtaxConfig.class);
}
}
package com.greatchn.etax;
/**
* 电子税务局方法执行结果
*
* @param success
* @param message
* @param result
* @param <T>
*/
public record FunctionResult<T>(boolean success, String message, T result) {
}
package com.greatchn.etax.exceptions;
/**
* EtaxException
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
public class EtaxException extends RuntimeException {
public EtaxException(String message) {
super(message);
}
public EtaxException(String message, Throwable cause) {
super(message, cause);
}
}
package com.greatchn.etax.exceptions;
/**
* 重试建议异常
*
* @author LWang 2023.02.01
* @since 1.0.0
*/
public class RetryWarning extends RuntimeException {
/**
*
*/
private final long retryDelay;
/**
* 重试警告异常
*
* @param retryDelay 重试延时(单位:分钟)
* @param message 异常消息
*/
public RetryWarning(long retryDelay, String message) {
super(message);
this.retryDelay = retryDelay;
}
public long getRetryDelay() {
return retryDelay;
}
}
package com.greatchn.etax.monitor;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.TimeUnit;
/**
* 任务监视器
*
* @author LWang 2023.02.03
* @since 1.0.0
*/
public class Monitor extends JFrame {
// 内部组件
/**
* 运行实例 ID Label
*/
private JLabel instanceLabel;
/**
* 用户 ID Label
*/
private JLabel userLabel;
/**
* 任务 ID Label
*/
private JLabel taskLabel;
/**
* 日志 label
*/
private JLabel logLabel;
/**
* 关闭按钮
*/
private JButton closeButton;
private final int width = 960;
private final int height = 64;
/**
* 任务监视器启动服务
*/
public Monitor() {
JFrame.setDefaultLookAndFeelDecorated(false);
this.setLayout(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setPreferredSize(new Dimension(width, height));
this.setUndecorated(true);
this.getRootPane().setWindowDecorationStyle(JRootPane.NONE);
this.setLocation(0, 500);
this.setAlwaysOnTop(true);
this.addComponent();
this.pack();
this.setVisible(true);
}
private final String instanceLabelTextPatter = "当前实例编号:%s";
private final String userLabelTextPatter = "当前任务用户:%s";
private final String taskLabelTextPatter = "当前任务 ID:%s";
private final String logLabelTextPatter = "任务日志:%s";
private void addComponent() {
final var startX = 10;
final var startY = 5;
final var padding = 5;
final var font = new Font("Microsoft YaHei UI", Font.PLAIN, 12);
// 使用绝对定位布局
this.setBounds(0, 0, width, height);
var x = startX;
var y = startY;
this.getContentPane().setBackground(Color.getHSBColor(0.5000f, 0.1667f, 0.4706f));
// 第一行:
this.instanceLabel = new JLabel(instanceLabelTextPatter.formatted(StringUtils.EMPTY));
this.instanceLabel.setBounds(x, y, 320, 22);
this.instanceLabel.setForeground(Color.WHITE);
this.instanceLabel.setFont(font);
this.getContentPane().add(this.instanceLabel);
x += this.instanceLabel.getWidth() + padding;
this.userLabel = new JLabel(userLabelTextPatter.formatted(StringUtils.EMPTY));
this.userLabel.setBounds(x, y, 240, 22);
this.userLabel.setForeground(Color.WHITE);
this.userLabel.setFont(font);
this.getContentPane().add(this.userLabel);
x += this.userLabel.getWidth() + padding;
this.taskLabel = new JLabel(taskLabelTextPatter.formatted(StringUtils.EMPTY));
this.taskLabel.setBounds(x, y, 240, 22);
this.taskLabel.setForeground(Color.WHITE);
this.taskLabel.setFont(font);
this.getContentPane().add(this.taskLabel);
x += this.taskLabel.getWidth() + padding * 2;
this.closeButton = new JButton("关闭服务");
this.closeButton.setBounds(x, y, 120, 54);
this.closeButton.setFont(font);
this.closeButton.setBackground(Color.getHSBColor(0.5986f, 0.5506f, 0.3490f));
this.closeButton.setForeground(Color.WHITE);
this.closeButton.setDefaultCapable(false);
this.closeButton.setBorderPainted(false);
this.closeButton.setFocusPainted(false);
this.closeButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
this.closeButton.addActionListener(e -> exit());
this.getContentPane().add(this.closeButton);
x = startX;
y += 32;
this.logLabel = new JLabel(logLabelTextPatter.formatted(StringUtils.EMPTY));
this.logLabel.setBounds(x, y, 810, 22);
this.logLabel.setForeground(Color.WHITE);
this.logLabel.setFont(font);
this.getContentPane().add(this.logLabel);
}
private boolean doNext = true;
public boolean doNext() {
return this.doNext;
}
/**
* 退出处理
*/
public void exit() {
this.doNext = false;
this.dispose();
}
/**
* @param userId 用户 ID
*/
public void setUserId(String userId) {
userLabel.setText(userLabelTextPatter.formatted(userId));
}
/**
* @param taskId 任务 ID
*/
public void setTaskId(String taskId) {
taskLabel.setText(taskLabelTextPatter.formatted(taskId));
}
/**
* 设置日志
*
* @param log 日志信息
*/
public void setLog(String log) {
logLabel.setText(logLabelTextPatter.formatted(log));
}
public static void main(String[] args) throws InterruptedException {
var monitor = new Monitor();
while (monitor.doNext()) {
monitor.setTaskId(String.valueOf(RandomUtils.nextLong(0, 10000)));
monitor.setUserId(RandomStringUtils.random(24, "甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳无为申酉戌亥"));
monitor.setLog(RandomStringUtils.random(100, "甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳无为申酉戌亥"));
TimeUnit.SECONDS.sleep(2);
}
System.exit(0);
}
}
package com.greatchn.etax.sms;
import com.greatchn.etax.sms.bean.SmsResult;
import com.greatchn.etax.sms.exceptions.SmsException;
import com.greatchn.kits.HttpTool;
import com.greatchn.kits.JacksonKits;
import com.greatchn.kits.YamlKits;
import lombok.Data;
import org.apache.commons.lang3.tuple.Pair;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 短信管理服务
*
* @author LWang 2023.01.30
* @since 1.0.0
*/
public final class SmsBiz {
private SmsBiz() {
}
/**
* 短信处理配置类
*
* @author LWang 2023.02.01
* @since 1.0.0
*/
@Data
public static class SmsConfig {
/**
* 短信服务地址
*/
private String url;
}
/**
* 短信发送处理接口
*/
public interface SendShortMessageExecutor {
/**
* 发送短信功能
*/
void execute();
}
/**
* 短信类型枚举
*
* @author LWang 2023.01.30
* @since 1.0.0
*/
public enum SmsTaskType {
/**
* 天津市电子税务局验证码短信
*/
ETAX_TIANJIN("TJ_ETAX"),
/**
* 测试类型的短信
*/
TEST("TEST");
final String type;
public String getType() {
return type;
}
SmsTaskType(String type) {
this.type = type;
}
}
private final static SmsConfig CONFIG;
private final static HttpTool HTTP_TOOL;
static {
CONFIG = YamlKits.load("sms.yaml", SmsConfig.class);
try {
HTTP_TOOL = HttpTool.build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
/**
* 添加读取短信任务
*
* @param phone 手机号
* @return 添加结果
*/
private static SmsResult addTask(String phone, SmsTaskType type) throws IOException, InterruptedException {
var url = String.format("%s/monitor_task/add", CONFIG.url);
var response = HTTP_TOOL.post(url, new ArrayList<>() {{
this.add(Pair.of("data", String.format("""
{
"phone": "%s",
"taskType": "%s"
}
""", phone, type.getType()
)
));
}});
if (response.getKey() != 200) {
return new SmsResult(SmsResult.UNKNOWN, "网络错误!", null);
}
return JacksonKits.toBean(response.getValue(), SmsResult.class);
}
/**
* 读取短信任务结果
*
* @param taskId 任务 ID
* @return 读取结果
*/
private static SmsResult readTask(String taskId) throws IOException, InterruptedException {
var url = String.format("%s/monitor_task/query", CONFIG.url);
var response = HTTP_TOOL.post(url, new ArrayList<>() {{
this.add(Pair.of("task_id", taskId));
}});
if (response.getKey() != 200) {
return new SmsResult(SmsResult.UNKNOWN, "网络错误!", null);
}
return JacksonKits.toBean(response.getValue(), SmsResult.class);
}
/**
* 取消短信任务
*
* @param taskId 任务 ID
* @return 读取结果
*/
private static SmsResult cancelTask(String taskId) throws IOException, InterruptedException {
var url = String.format("%s/monitor_task/cancel", CONFIG.url);
var response = HTTP_TOOL.post(url, new ArrayList<>() {{
this.add(Pair.of("task_id", taskId));
}});
if (response.getKey() != 200) {
return new SmsResult(SmsResult.UNKNOWN, "网络错误!", null);
}
return JacksonKits.toBean(response.getValue(), SmsResult.class);
}
/**
* 等待验证码
*
* @param phone 手机号
* @param type 验证码类型
* @param executor 执行器
* @return 验证码
*/
public static String waitSmsCode(String phone, SmsTaskType type, SendShortMessageExecutor executor) {
try {
var taskId = SmsResult.handle(addTask(phone, type)).data().taskId();
try {
executor.execute();
var queryResult = SmsResult.handle(readTask(taskId));
var retryTimes = 12;
while (queryResult.code() == SmsResult.PROCESSING) {
queryResult = SmsResult.handle(readTask(taskId));
if (retryTimes-- <= 0) {
throw new SmsException(SmsException.NET_ERROR, "获取验证码超时!");
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException ignore) {
}
}
return queryResult.data().validateCode();
} finally {
cancelTask(taskId);
}
} catch (IOException | InterruptedException e) {
throw new SmsException(SmsException.NET_ERROR, "短信服务异常,任务无法继续!");
}
}
public static void main(String[] args) {
waitSmsCode("18920961634", SmsTaskType.TEST, () -> System.out.println("发短信喽……"));
}
}
package com.greatchn.etax.sms.bean;
import com.fasterxml.jackson.annotation.JsonProperty;
public record SmsCode(
String taskId,
@JsonProperty("SMSContent")
String smsContent,
String validateCode,
String phone
) {
}
package com.greatchn.etax.sms.bean;
import com.greatchn.etax.sms.exceptions.SmsException;
import java.util.Objects;
/**
* 短信服务响应码
*
* @author LWang 2023.02.01
* @since 1.0.0
*/
public record SmsResult(
int code,
String message,
SmsCode data
) {
/**
* 响应码:成功 {@value }
*/
public final static int SUCCESS = 200;
/**
* 响应码:执行中 {@value }
*/
public final static int PROCESSING = 201;
/**
* 响应码:任务重复 {@value }
*/
public final static int REPEAT = 401;
/**
* 响应码:超过每日限额 {@value }
*/
public final static int LIMIT = 402;
/**
* 响应码:任务不存在 {@value }
*/
public final static int NONEXISTENT = 404;
/**
* 响应码未知异常 {@value }
*/
public final static int UNKNOWN = 499;
public static SmsResult handle(SmsResult result) {
if (Objects.isNull(result)) {
throw new IllegalArgumentException("缺少参数:result");
}
switch (result.code) {
case REPEAT, NONEXISTENT -> throw new SmsException(SmsException.PHONE_LOCK, result.message);
case LIMIT -> throw new SmsException(SmsException.PHONE_LIMIT_RANGE, result.message);
case UNKNOWN -> throw new SmsException(SmsException.NET_ERROR, result.message);
}
return result;
}
}
package com.greatchn.etax.sms.exceptions;
/**
* 短信异常
*
* @author LWang 2023.01.30
* @since 1.0.0
*/
public class SmsException extends RuntimeException {
/**
* 短信异常,手机属于锁定状态(建议任务延迟 5 分钟)
*/
public static final int PHONE_LOCK = 1;
/**
* 短信异常,电子税务局时间范围内不可再接收短信(建议任务延迟 15 分钟)
*/
public static final int PHONE_LIMIT_RANGE = 2;
/**
* 短信异常,电子税务局担心当日可用量已满(建议任务延迟到转日)
*/
public static final int PHONE_FULL_DAY = 3;
/**
* 短信异常,网络错误(建议任务延迟 5 分钟)
*/
public static final int NET_ERROR = 0;
private final int flag;
public SmsException(int flag, String message) {
super(message);
this.flag = flag;
}
/**
* 获取异常标志
*
* @return 异常标志
*/
public int getFlag() {
return flag;
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.EtaxConfig;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.etax.tianjin.bean.SysApiResponse;
import com.greatchn.kits.RandomKits;
import com.greatchn.rpa.beans.Point;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
/**
* 自然人登录(TODO 后期完善)
*
* @author LWang 2023.01.30
* @since 1.0.0
*/
@Slf4j
public class HumanLoginFunction extends LoginFunction {
public HumanLoginFunction(BaseEtax etax) {
super(etax);
}
/**
* 自然人登录
*
* @param personId 自然人账号
* @param personPassword 自然人密码
* @return 登录结果
*/
public FunctionResult<Object> login(String personId, String personPassword) {
config().setLoginType(EtaxConfig.LOGIN_TYPE_HUMAN);
try {
rpa().visit(config().getMainUrl());
tryLogout();
rpa().visit(config().getDomain());
tryCloseNotices();
toLoginPage();
tryLogin(personId, personPassword);
ignoreSetPassword();
rpa().sleep(SLEEP_TIME);
if (!StringUtils.contains(rpa().url(), config().getHomeUri())) {
throw new EtaxException("出现未预料的登录流程!");
}
return new FunctionResult<>(true, "登录成功。", null);
} catch (Exception e) {
return handleException(e, "登录失败,");
}
}
/**
* 尝试进行自然人登录
*
* @param personId 自然人账号
* @param personPassword 自然人密码
*/
protected void tryLogin(String personId, String personPassword) {
rpa().click(rpa().findElementWithInnerText("自然人业务", By.cssSelector("div.justifyCenterEnd > span"), WAIT_TIME));
rpa().click(rpa().findElementWithInnerText("用户名登录", By.cssSelector("div.card > div"), WAIT_TIME));
// 8. 输入账号,密码
rpa().sendKey(By.cssSelector("input[placeholder=\"用户名\"]"), personId, true, WAIT_TIME);
rpa().sendKey(By.cssSelector("input[placeholder=\"个人用户密码(初始密码为证件号码后六位)\"]"), personPassword, true, WAIT_TIME);
// 9. 滑动验证
var dragHandler = rpa().findElement(By.cssSelector("div.drag > div.handler"), WAIT_TIME);
rpa().moveTo(dragHandler);
var drag = rpa().findElement(By.cssSelector("div.drag"));
var handlerRect = rpa().sumElementRectangle(drag);
rpa().dragTo(
new Point(
RandomKits.nextDouble(handlerRect.rightTop().x(), handlerRect.rightTop().x() + MOUSE_OFFSET),
RandomKits.nextDouble(handlerRect.rightTop().y() - MOUSE_OFFSET, handlerRect.rightBottom().y()) + MOUSE_OFFSET
)
);
// 10. 登录处理(监听登录 Ajax 结果)
rpa().newHar();
rpa().click(By.cssSelector("button.el-button.loginCls"), WAIT_TIME);
var response = rpa().jsonListen("auth/user/single/userNamePswLogin", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.etax.sms.SmsBiz;
import com.greatchn.etax.tianjin.bean.SysApiResponse;
import com.greatchn.kits.RandomKits;
import com.greatchn.rpa.beans.Point;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
/**
* 电子税务局身份认证处理方法
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public class IdentityAuthenticationFunction extends BaseFunction {
public IdentityAuthenticationFunction(BaseEtax etax) {
super(etax);
}
/**
* 验证是否需要进行身份验证
*
* @return true 需要身份验证,false 不需要身份验证
*/
public boolean needIdentityAuthentication() {
try {
rpa().findElementWithInnerText("身份验证", By.id("dialog1title"), WAIT_TIME / 2);
return true;
} catch (NoSuchElementException e) {
return false;
}
}
/**
* 短信超限消息
*/
private static final String LIMIT_MESSAGE = "短信发送次数超限!";
public FunctionResult<Object> identityAuthentication() {
var currentUrl = rpa().url();
try {
var sessionUser = etax().function(SessionFunction.class).getSessionUser(false);
var targetUrl = rpa().findElement(By.id("ecrzkxDxId")).getAttribute("src");
// 因为跨域了,所以要将身份识别的 iframe 提升到顶级窗口
rpa().visit(targetUrl);
// 先进行滑动验证
var dragHandler = rpa().findElement(By.cssSelector("div.drag > div.handler"), WAIT_TIME);
rpa().moveTo(dragHandler);
var drag = rpa().findElement(By.cssSelector("div.drag"));
var handlerRect = rpa().sumElementRectangle(drag);
rpa().dragTo(
new Point(
RandomKits.nextDouble(handlerRect.rightTop().x(), handlerRect.rightTop().x() + MOUSE_OFFSET),
RandomKits.nextDouble(handlerRect.rightTop().y() - MOUSE_OFFSET, handlerRect.rightBottom().y()) + MOUSE_OFFSET
)
);
var code = SmsBiz.waitSmsCode(sessionUser.sjh(), SmsBiz.SmsTaskType.ETAX_TIANJIN, () -> {
rpa().newHar();
rpa().findElementWithInnerText("获取验证码", By.cssSelector("button.el-button"), WAIT_TIME).click();
var response = rpa().jsonListen("auth/oauth2/sendSmsByToke", SysApiResponse.class);
if (!response.success()) {
if (response.msg().contains(LIMIT_MESSAGE)) {
throw new EtaxException(response.msg());
}
}
});
rpa().sendKey(By.cssSelector("input.el-input__inner[placeholder=\"请输入短信验证码\"]"), code, false, WAIT_TIME);
rpa().newHar();
rpa().findElementWithInnerText("确定", By.cssSelector("button.el-button"), WAIT_TIME).click();
var response = rpa().jsonListen("auth/oauth2/verifySmsCode", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
return new FunctionResult<>(true, "身份认证成功。", null);
} catch (Exception e) {
return handleException(e, "身份认证失败,");
} finally {
rpa().visit(currentUrl);
}
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.EtaxConfig;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.etax.tianjin.bean.SysApiResponse;
import com.greatchn.kits.RandomKits;
import com.greatchn.rpa.beans.Point;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
/**
* 登录方法
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public class LoginFunction extends BaseFunction {
public LoginFunction(BaseEtax etax) {
super(etax);
}
/**
* 天津市电子税务局登录
*
* @param taxpayerId 税号
* @param personId 办税人 ID
* @param personPassword 办税人密码
*/
protected FunctionResult<Object> login(String taxpayerId, String personId, String personPassword) {
config().setLoginType(EtaxConfig.LOGIN_TYPE_ENTERPRISE);
try {
rpa().visit(config().getMainUrl());
tryLogout();
rpa().visit(config().getDomain());
tryCloseNotices();
toLoginPage();
tryLogin(taxpayerId, personId, personPassword);
rpa().sleep(SLEEP_TIME);
choiceRole();
rpa().sleep(SLEEP_TIME);
acceptJoin();
rpa().sleep(SLEEP_TIME);
ignoreSetPassword();
rpa().sleep(SLEEP_TIME);
if (!StringUtils.contains(rpa().url(), config().getHomeUri())) {
throw new EtaxException("出现未预料的登录流程!");
}
// 尝试关闭通知
return new FunctionResult<>(true, "登录成功。", null);
} catch (Exception e) {
return handleException(e, "登录失败,");
}
}
/**
* 忽略修改密码处理
*/
protected void ignoreSetPassword() {
if (StringUtils.contains(rpa().url(), config().getHomeUri())) {
return;
}
try {
rpa().waitElement(By.cssSelector("input.el-input__inner[placeholder=\"请设置密码\"]"), WAIT_TIME);
rpa().refresh();
} catch (NoSuchElementException ignore) {
}
}
/**
* 角色加入确认
*/
protected void acceptJoin() {
if (StringUtils.contains(rpa().url(), config().getHomeUri())) {
return;
}
try {
var dialogs = rpa().findElements(By.cssSelector(".el-message-box__wrapper .el-message-box"), WAIT_TIME);
dialogs.forEach(dialog -> {
if (rpa().inElementText(rpa().findElement(dialog, By.cssSelector(".el-message-box__container")), "")) {
rpa().click(rpa().findElementWithInnerText("确定", dialog, By.cssSelector(".el-message-box__btns button.el-button")));
}
});
} catch (NoSuchElementException ignore) {
}
}
/**
* 选择角色
*/
protected void choiceRole() {
if (StringUtils.contains(rpa().url(), config().getHomeUri())) {
return;
}
// 12. 如果没有进入电子税务局首页,判断是否有角色选择处理
try {
rpa().newHar();
var choice = false;
for (var role : etax().getRoles()) {
try {
rpa().click(rpa().findElementWithInnerText(role, By.cssSelector(".el-radio .el-radio__label"), WAIT_TIME));
choice = true;
break;
} catch (NoSuchElementException ignore) {
}
}
if (!choice) {
throw new EtaxException("无法识别的角色!");
}
rpa().click(By.cssSelector("div.el-dialog[aria-label=\"身份类型选择\"] button.el-button.el-button--primary"), WAIT_TIME);
var response = rpa().jsonListen("auth/user/agreementListQuery", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
} catch (NoSuchElementException ignore) {
}
}
/**
* 尝试登录
*
* @param taxpayerId 税号
* @param personId 办税人账号
* @param personPassword 办税人密码
*/
protected void tryLogin(String taxpayerId, String personId, String personPassword) {
// 7. 切换到 企业业务 - 快捷登录 模式
rpa().click(rpa().findElementWithInnerText("企业业务", By.cssSelector("div.justifyCenterEnd > span"), WAIT_TIME));
rpa().click(rpa().findElementWithInnerText("快捷登录", By.cssSelector("div.card > div"), WAIT_TIME));
// 8. 输入账号,密码
rpa().sendKey(By.cssSelector("input[placeholder=\"统一社会信用代码/纳税人识别号\"]"), taxpayerId, true, WAIT_TIME);
rpa().sendKey(By.cssSelector("input[placeholder=\"居民身份证号码/手机号码/用户名\"]"), personId, true, WAIT_TIME);
rpa().sendKey(By.cssSelector("input[placeholder=\"个人用户密码(初始密码为证件号码后六位)\"]"), personPassword, true, WAIT_TIME);
// 9. 滑动验证
var dragHandler = rpa().findElement(By.cssSelector("div.drag > div.handler"), WAIT_TIME);
rpa().moveTo(dragHandler);
var drag = rpa().findElement(By.cssSelector("div.drag"));
var handlerRect = rpa().sumElementRectangle(drag);
rpa().dragTo(
new Point(
RandomKits.nextDouble(handlerRect.rightTop().x(), handlerRect.rightTop().x() + MOUSE_OFFSET),
RandomKits.nextDouble(handlerRect.rightTop().y() - MOUSE_OFFSET, handlerRect.rightBottom().y()) + MOUSE_OFFSET
)
);
// 10. 登录处理(监听登录 Ajax 结果)
rpa().newHar();
rpa().click(By.cssSelector("button.el-button.loginCls"), WAIT_TIME);
var response = rpa().jsonListen("auth/enterprise/quick/accountLogin", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
}
/**
* 访问登录页面
*/
protected void toLoginPage() {
try {
var loginButton = rpa().findElementWithInnerText("新版登录", By.cssSelector(".login-font"), WAIT_TIME);
rpa().click(loginButton);
} catch (NoSuchElementException e) {
// 6. 找不到登录按钮,通过电子税务局内部脚本获取登录地址
var loginUrl = rpa().executeJavaScript("return WSSW.kxLoingUrl", String.class);
if (StringUtils.isBlank(loginUrl)) {
throw new EtaxException("无法进入登录页面。");
}
rpa().visit(loginUrl);
}
}
/**
* 尝试关闭所有通知
*/
protected void tryCloseNotices() {
// 4. 尝试关闭所有通知框
try {
rpa().findElements(By.cssSelector("div.noticeContainer div.notice span.known"), WAIT_TIME).forEach(WebElement::click);
} catch (NoSuchElementException ignored) {
// 尝试性操作异常不需要处理
}
}
/**
* 尝试退出操作
*/
protected void tryLogout() {
// 2. 尝试进行退出操作
try {
rpa().waitElement(By.cssSelector("div.user span.userName span.userNameInfo"), WAIT_TIME);
// 通过脚本进行退出操作
rpa().newHar();
rpa().executeJavaScript("""
(function() {
try {
WSSW.index.outWSSW();
} catch(e) {
}
})();
""", Object.class);
rpa().listen("apps/view/login.html");
} catch (NoSuchElementException ignored) {
// 尝试性操作异常不需要处理
}
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.EtaxConfig;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import java.util.Objects;
/**
* 主菜单管理方法
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public class MenuFunction extends BaseFunction {
public MenuFunction(BaseEtax etax) {
super(etax);
}
/**
* 切换电子税务局主页菜单
*
* @param type 功能类型
* @param menu 功能名称
* @return 切换结果
*/
public FunctionResult<Object> choiceMainMenu(String type, String menu) {
// 1. 返回电子税务局首页
rpa().visit(config().getMainUrl());
try {
// 判断是否存在 divAlertBox 对话框,内容为
rpa().findElementWithInnerText("用户未登录!", By.cssSelector("#divAlertBox span"), WAIT_TIME / 2);
return new FunctionResult<>(false, "切换菜单失败,用户未登录!", null);
} catch (NoSuchElementException ignore) {
// 继续执行
}
// 切换菜单类型
try {
var typeTab = rpa().findElementWithInnerText(type, By.cssSelector("div.main-rightPane-topPane li[role=\"presentation\"] a[role=\"tab\"]"), WAIT_TIME);
rpa().click(typeTab);
if (config().getLoginType() == EtaxConfig.LOGIN_TYPE_ENTERPRISE) {
// 切换到菜单 iFrame
var typeId = typeTab.getAttribute("aria-controls");
rpa().switchToFrame(By.cssSelector("div#" + typeId + " iframe"), WAIT_TIME);
}
try {
rpa().click(rpa().findElementWithInnerText(menu, By.cssSelector("li.menuContainerLi span.menuContainerLi-span"), WAIT_TIME / 2));
} catch (Exception e) {
rpa().click(rpa().findElementWithInnerText(menu, By.cssSelector("li.menuContainerLi-file span.menuContainerLi-span"), WAIT_TIME / 2));
}
return new FunctionResult<>(true, "菜单切换成功。", null);
} catch (Exception e) {
return handleException(e, "菜单切换失败,");
} finally {
rpa().switchToParent();
}
}
/**
* 切换账户中心菜单
*
* @param first 一级菜单
* @param second 二级菜单
* @return 切换结果
*/
public FunctionResult<Object> choiceAccountCenterMenu(String first, String second) {
try {
var header = rpa().findElement(By.cssSelector(".content .sidebar-container .sidebar-head"));
var headerText = "账户中心";
if (!rpa().textEquals(header, headerText)) {
return new FunctionResult<>(false, "切换账户中心菜单失败,请先进入该功能!", null);
}
} catch (NoSuchElementException e) {
return new FunctionResult<>(false, "切换账户中心菜单失败," + e.getMessage(), null);
}
try {
// 切换一级菜单
rpa().findElementWithInnerText(first, By.cssSelector("li.el-submenu span"), WAIT_TIME).click();
// 切换二级菜单
rpa().findElement(By.cssSelector("li.el-menu-item[title=\"" + second + "\"]"), WAIT_TIME).click();
return new FunctionResult<>(true, "切换账户中心菜单成功。", null);
} catch (Exception e) {
return handleException(e, "切换账户中心菜单失败,");
}
}
/**
* 菜单标志:点 - 功能菜单
*/
private static final String MENU_DOT = "•";
/**
* 选择目录菜单
*
* @param menu 菜单内容
* @param needFolder true:目录菜单;false:文件菜单
* @return 菜单元素
*/
private WebElement choiceMenu(String menu, boolean needFolder) {
var menuElements = rpa().findElements(By.cssSelector(".leftPanel-li-div"), WAIT_TIME);
for (var menuElement : menuElements) {
var point = menuElement.findElement(By.cssSelector("span.leftPane-pointer"));
var isFile = rpa().textEquals(point, MENU_DOT);
if (needFolder ^ isFile) {
var spans = menuElement.findElements(By.tagName("span"));
for (var span : spans) {
if (rpa().textEquals(span, menu)) {
return span;
}
}
}
}
return null;
}
/**
* 进入功能菜单
*
* @param menus 菜单顺序
* @return 执行结果
*/
public FunctionResult<Object> choiceFunctionMenu(String... menus) {
if (Objects.isNull(menus) || menus.length == 0) {
return new FunctionResult<>(false, "功能菜单选择失败,未指定要进入的功能", null);
}
try {
for (var i = 0; i < menus.length; i++) {
WebElement menu;
if (i == menus.length - 1) {
menu = choiceMenu(menus[i], false);
} else {
menu = choiceMenu(menus[i], true);
}
if (Objects.isNull(menu)) {
throw new EtaxException(menus[i] + "不存在!");
} else {
menu.click();
}
}
// 身份识别判断
var functionObj = etax().function(IdentityAuthenticationFunction.class);
if (functionObj.needIdentityAuthentication()) {
var result = etax().function(IdentityAuthenticationFunction.class).identityAuthentication();
if (result.success()) {
return choiceFunctionMenu(menus);
} else {
throw new EtaxException(result.message());
}
}
// 是否可以使用判断
try {
var dialog = rpa().findElement(By.id("divAlertBox"), WAIT_TIME);
if (!Objects.isNull(dialog)) {
throw new EtaxException(StringUtils.trim(dialog.getText()));
}
} catch (Exception ignore) {
}
return new FunctionResult<>(true, "切换功能菜单成功。", null);
} catch (Exception e) {
return handleException(e, "切换功能菜单失败,");
}
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.etax.tianjin.bean.SysApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import java.util.ArrayList;
import java.util.List;
/**
* 查询绑定企业 列表
*
* @author LWang 2023.01.30
* @since 1.0.0
*/
@Slf4j
public class QueryBindEnterpriseFunction extends BaseFunction {
public QueryBindEnterpriseFunction(BaseEtax etax) {
super(etax);
}
public FunctionResult<List<String>> queryBindEnterprise() {
// 切换到 用户中心 - 自然人账户中心
var menuFunctionObj = etax().function(MenuFunction.class);
var choiceMainMenuResult = menuFunctionObj.choiceMainMenu("用户中心", "自然人账户中心");
if (!choiceMainMenuResult.success()) {
return new FunctionResult<>(false, choiceMainMenuResult.message(), null);
}
var choiceAccountCenterMenuResult = menuFunctionObj.choiceAccountCenterMenu("企业授权管理", "已授权企业");
if (!choiceAccountCenterMenuResult.success()) {
return new FunctionResult<>(false, choiceAccountCenterMenuResult.message(), null);
}
try {
rpa().findElementWithInnerText("已授权企业", By.cssSelector("div.s_Breadcrumb"), WAIT_TIME);
var list = new ArrayList<String>();
// 每页长度改为 40
var page = rpa().findElement(By.cssSelector(".el-pagination .el-pagination__sizes .el-select"), WAIT_TIME);
rpa().click(page);
rpa().newHar();
rpa().click(rpa().findElementWithInnerText("40条/页", By.cssSelector(".el-select-dropdown__wrap .el-select-dropdown__item span"), WAIT_TIME));
var response = rpa().jsonListen("idm/internal/relation/selectRelationList", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
// 查询列表
while (true) {
var rows = rpa().findElements(By.cssSelector(".el-table__body-wrapper .el-table__row"), WAIT_TIME);
for (var row : rows) {
var cells = row.findElements(By.tagName("td"));
var line = new ArrayList<String>(cells.size());
for (var cell : cells) {
line.add(cell.getText());
}
list.add(StringUtils.join(line, "\t"));
}
// 读取下一页按钮
var nextPage = rpa().findElement(By.cssSelector(".btn-next"), WAIT_TIME);
if (!nextPage.isEnabled()) {
break;
}
rpa().newHar();
nextPage.click();
response = rpa().jsonListen("idm/internal/relation/selectRelationList", SysApiResponse.class);
if (!response.success()) {
throw new EtaxException(response.msg());
}
rpa().sleep(1000);
}
return new FunctionResult<>(true, "OK", list);
} catch (Exception e) {
return handleException(e, "查询绑定企业失败,");
}
}
}
package com.greatchn.etax.tianjin;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import com.greatchn.kits.DateTimeKits;
import com.greatchn.kits.HttpTool;
import com.greatchn.kits.JacksonKits;
import com.greatchn.kits.oss.OssKits;
import lombok.Data;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* 查询完税证明方法
*
* @author LWang 2023.02.02
* @since 1.0.0
*/
public class QueryDutyPaidProofFunction extends BaseFunction {
public QueryDutyPaidProofFunction(BaseEtax etax) {
super(etax);
}
/**
* 查询完税凭证
*
* @param taxpayerId 纳税人登记序号
* @param startDate 开始时间
* @param endDate 结束时间
* @return 保存到 OSS 中的文件 ID
*/
public FunctionResult<String> queryDutyPaidProof(String taxpayerId, String startDate, String endDate) {
// 进入功能处理流程
try {
var menuFunction = etax().function(MenuFunction.class);
// 切换主菜单
var choiceMainMenuResult = menuFunction.choiceMainMenu("我要办税", "证明开具");
if (!choiceMainMenuResult.success()) {
return new FunctionResult<>(false, choiceMainMenuResult.message(), null);
}
// 切换功能菜单
var choiceFunctionMenuResult = menuFunction.choiceFunctionMenu("开具税收完税(费)证明(文书式)");
if (!choiceFunctionMenuResult.success()) {
return new FunctionResult<>(false, choiceFunctionMenuResult.message(), null);
}
// 切换进入实际工作 iFrame
rpa().switchToFrame(By.id("iframeContainerPaneId"), WAIT_TIME);
// 选择企业,并下一步进行查询
rpa().click(By.cssSelector("input[type=\"radio\"][value=\"%s\"]".formatted(taxpayerId)), WAIT_TIME);
rpa().click(By.cssSelector("input.next[type=\"button\"][value=\"下一步\"]"), WAIT_TIME);
// 设置查询条件
rpa().click(By.cssSelector("input#dygs[value=\"1\"]"), WAIT_TIME);
rpa().click(By.id("nspzhkzl"), WAIT_TIME);
rpa().findElementWithInnerText("税收完税证明(文书式)", By.cssSelector("select#nspzhkzl option"), WAIT_TIME).click();
rpa().executeJavaScript("document.querySelector('#skssqq').value=arguments[0]", Object.class, startDate);
rpa().executeJavaScript("document.querySelector('#skssqz').value=arguments[0]", Object.class, endDate);
rpa().executeJavaScript("""
(function() {
try {
window.alert = function() {}
window.confirm = function() {return true;}
} catch (e) {
}
})();
""", Object.class);
rpa().newHar();
rpa().click(By.cssSelector("input.last[value=\"查询\"]"), WAIT_TIME);
var response = rpa().jsonListen("WsbsWebBn/wsbsWszmAction_queryJks.do");
var code = response.get("returnCode").asText(CODE_ERROR);
var rows = response.get("rows");
if (!Objects.equals(code, CODE_SUCCESS)) {
throw new EtaxException("未查询到符合要求的数据,请检查对应的税款是否入库");
}
if (rows instanceof ArrayNode arrayNode && arrayNode.size() <= 0) {
throw new EtaxException("未查询到符合要求的数据,请检查对应的税款是否入库");
}
// 替换页面中的 savDataCallback 方法,不执行页面跳转操作
rpa().executeJavaScript("window.savDataCallback = function(applicationId){}", Object.class);
var sn = StringUtils.EMPTY;
var submit = rpa().findElement(By.cssSelector("input.next[value=\"下一步\"]"));
while (StringUtils.isBlank(sn)) {
rpa().newHar();
submit.click();
response = rpa().jsonListen("WsbsWebBn/wsbsWszmAction_saveWszmxx.do");
if (Objects.isNull(response)) {
throw new EtaxException("开具完税证明过程出现异常,请重新登录!");
}
var result = response.get("result");
if (Objects.isNull(result)) {
throw new EtaxException("开具完税证明过程出现异常,请重新登录!");
}
var saveResult = JacksonKits.toBean(result.asText("{}"), SaveResult.class);
assert saveResult != null;
if (Objects.equals("modify", saveResult.flag)) {
throw new EtaxException("请勿篡改完税信息!");
}
if (Objects.equals("session", saveResult.flag)) {
throw new EtaxException("当前登录用户异常,请重新登录!");
}
if (Objects.equals("N", saveResult.flag)) {
sn = saveResult.lsh;
break;
}
rpa().sleep(SLEEP_TIME);
}
// 使用流水号查询完税证明
return downloadDutyPaidProof(sn);
} catch (Exception e) {
return handleException(e, "查询完税证明失败,");
} finally {
rpa().switchToParent();
}
}
/**
* 下载完税凭证
*
* @param sn 流水号
* @return 保存在 SSO 中的文件编号
*/
private FunctionResult<String> downloadDutyPaidProof(String sn) throws NoSuchAlgorithmException, IOException, InterruptedException, KeyManagementException, URISyntaxException {
rpa().switchToParent();
var menuFunction = etax().function(MenuFunction.class);
// 切换主菜单
var choiceMainMenuResult = menuFunction.choiceMainMenu("我要办税", "证明开具");
if (!choiceMainMenuResult.success()) {
return new FunctionResult<>(false, choiceMainMenuResult.message(), null);
}
// 切换功能菜单
var choiceFunctionMenuResult = menuFunction.choiceFunctionMenu("查询已开具税收完税(费)证明(文书式)");
if (!choiceFunctionMenuResult.success()) {
return new FunctionResult<>(false, choiceFunctionMenuResult.message(), null);
}
rpa().switchToFrame(By.id("iframeContainerPaneId"), WAIT_TIME);
// 设置查询参数
rpa().sendKey(By.id("sqsjq"), "1970-01-01", true, WAIT_TIME);
rpa().sendKey(By.id("sqsjz"), DateTimeKits.currentDay(), true, WAIT_TIME);
rpa().sendKey(By.id("lsh"), sn, true, WAIT_TIME);
rpa().newHar();
rpa().findElement(By.cssSelector("input[type=\"button\"][value=\"查询\"]"), WAIT_TIME).click();
rpa().listen("wsbsWszmAction_queryYkjWszmxx.do");
// 查询表格
var rows = rpa().findElements(By.cssSelector("div.base table.table tbody tr"), WAIT_TIME);
for (var row : rows) {
WebElement downloadButton;
try {
downloadButton = row.findElement(By.cssSelector("input.applybtn[value=\"下载证明\"]"));
} catch (NoSuchElementException e) {
continue;
}
if (rpa().inElementText(row, sn)) {
var downloadInfo = downloadButton.getAttribute("onclick");
var matcher = Pattern.compile("download\\('(.+?)','(.+?)'\\)").matcher(downloadInfo);
if (matcher.find()) {
return new FunctionResult<>(true, "下载完成完税证明成功。", downloadPdf(matcher.group(1), matcher.group(2)));
} else {
throw new EtaxException("根据流水号下载晚熟凭证失败,流水号 " + sn + "对应的完税证明不存在!");
}
}
}
return null;
}
/**
* 下载完税凭证
*
* @param applicationId 应用 ID
* @param typeId 种类 ID
* @return OSS 资源 ID
*/
private String downloadPdf(String applicationId, String typeId) throws NoSuchAlgorithmException, KeyManagementException, IOException, InterruptedException {
var url = "%sWsbsWebBn/wsbsWszmAction_showPdf.do?applicationId=%s&zlId=%s".formatted(config().getDomain(), applicationId, typeId);
var httpClient = HttpTool.build();
var downloadInfo = httpClient.download(url, "pdf");
if (!downloadInfo.getKey().equals(HttpStatus.SC_OK)) {
throw new EtaxException("无法下载完税证明,对应的文件不存在!");
}
try {
return OssKits.upload(downloadInfo.getValue().toFile());
} finally {
FileUtils.delete(downloadInfo.getValue().toFile());
}
}
private final static String CODE_ERROR = "99";
private final static String CODE_SUCCESS = "00";
@Data
public static class SaveResult {
private String flag;
private String lsh;
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.tianjin.bean.SessionUser;
import com.greatchn.kits.JacksonKits;
import org.openqa.selenium.By;
/**
* 会话内容管理方法
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
public class SessionFunction extends BaseFunction {
public SessionFunction(BaseEtax etax) {
super(etax);
}
/**
* 获取当前登录用户信息
*
* @param refresh 是否刷新页面
* @return 登录用户信息
*/
public SessionUser getSessionUser(boolean refresh) {
try {
if (refresh) {
rpa().refresh();
rpa().sleep(SLEEP_TIME);
}
rpa().waitElement(By.cssSelector("div.user span.userName span.userNameInfo"), WAIT_TIME);
var val = rpa().executeJavaScript("""
return (function() {
try {
return JSON.stringify(WSSW.sessionUser);
} catch(e) {
return "{}";
}
})();
""", String.class);
return JacksonKits.toBean(val, SessionUser.class);
} catch (Exception ignore) {
}
return null;
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.BaseFunction;
import com.greatchn.etax.FunctionResult;
import com.greatchn.etax.exceptions.EtaxException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import java.util.Objects;
/**
* 企业切换管理方法
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public class SwitchTaxpayerFunction extends BaseFunction {
public SwitchTaxpayerFunction(BaseEtax etax) {
super(etax);
}
/**
* 切换企业
*
* @param taxFileNo 税号
* @return 切换结果
*/
public FunctionResult<Object> switchTaxpayer(String taxFileNo) {
// 1. 进入“我的信息” - “企业账户中心” 功能
{
var functionResult = etax().function(MenuFunction.class).choiceMainMenu("我的信息", "企业账户中心(代理账号中心)");
if (!functionResult.success()) {
return new FunctionResult<>(false, "切换企业失败," + functionResult.message(), null);
}
}
// 2. 进入“身份切换” - “企业办税” 功能
{
var functionResult = etax().function(MenuFunction.class).choiceAccountCenterMenu("身份切换", "企业办税");
if (!functionResult.success()) {
return new FunctionResult<>(false, "切换企业失败," + functionResult.message(), null);
}
}
try {
rpa().findElementWithInnerText("企业办税", By.cssSelector("div.s_Breadcrumb"), WAIT_TIME);
// 3. 切换到“企业企业切换”标签
rpa().click(By.id("tab-2"), WAIT_TIME);
// 4. 输入要切换到的税号
rpa().sendKey(By.cssSelector("input.el-input__inner[placeholder=\"请输入统一社会信用代码\"]"), taxFileNo, true, WAIT_TIME);
// 5. 查询,并等待结果返回
rpa().newHar();
rpa().findElementWithInnerText("查询", By.cssSelector("div.other-enterprise div.search-form button.el-button span"), WAIT_TIME).click();
rpa().listen("idm/internal/relation/selectRelationList");
// 6. 在结果列表中查找税号是否存在
var rows = rpa().findElements(By.cssSelector("div.other-enterprise table.el-table__body tr.el-table__row"), WAIT_TIME);
WebElement choiceButton = null;
for (var row : rows) {
var cells = row.findElements(By.cssSelector("td .cell"));
for (var cell : cells) {
if (rpa().textEquals(cell, taxFileNo)) {
choiceButton = row.findElement(By.cssSelector(".el-button.el-button--text.el-button--small"));
}
}
if (!Objects.isNull(choiceButton)) {
break;
}
}
// 7. 执行切换功能
if (Objects.isNull(choiceButton)) {
throw new EtaxException("当前用户未关联" + taxFileNo);
}
rpa().newHar();
choiceButton.click();
rpa().listen("idm/internal/relation/selectRelationList");
rpa().sleep(SLEEP_TIME);
// 判断 URL 是否是税务局首页,如果不是,查找角色选择对话框
if (!StringUtils.contains(rpa().url(), config().getHomeUri())) {
var dialog = rpa().findElement(By.cssSelector(".el-dialog.el-dialog--center"), WAIT_TIME);
var header = dialog.findElement(By.cssSelector(".el-dialog__title"));
var headerText = "身份类型选择";
if (rpa().textEquals(header, headerText)) {
var choice = false;
for (var role : etax().getRoles()) {
try {
rpa().click(rpa().findElementWithInnerText(role, By.cssSelector(".el-radio .el-radio__label"), WAIT_TIME));
choice = true;
break;
} catch (NoSuchElementException ignore) {
}
}
if (!choice) {
throw new EtaxException("无法识别的角色!");
}
dialog.findElement(By.cssSelector("button.el-button.el-button--primary")).click();
} else {
throw new EtaxException("出现未知的对话框信息," + header.getText());
}
}
// 8. 等待返回电子税务局首页
rpa().waitElement(By.cssSelector("div.user span.userName span.userNameInfo"), WAIT_TIME);
// 8.1 补充……因为电子税务局的 bug,需要清理电子税务局内部网上办税子系统的 Cookies
try {
rpa().removeCookies("WSBSSESSION");
} catch (Exception ignore) {
}
// 9. 返回结果
return new FunctionResult<>(true, "切换企业成功。", null);
} catch (Exception e) {
return handleException(e, "切换企业失败,发生未知异常!");
}
}
}
package com.greatchn.etax.tianjin;
import com.greatchn.etax.BaseEtax;
import com.greatchn.etax.FunctionResult;
import com.greatchn.rpa.config.RpaConfig;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Objects;
/**
* 天津市电子税务局交互处理对象
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
@Slf4j
public class Tianjin extends BaseEtax {
private static final String TIANJIN_CONFIG = "etax/tianjin.yaml";
/**
* 天津市电子税务局对象构造方法
*
* @param rpaConfig rpa 配置
*/
public Tianjin(RpaConfig rpaConfig) {
this(rpaConfig, TIANJIN_CONFIG);
}
/**
* 天津市电子税务局对象构造方法
*
* @param rpaConfig rpa 配置
* @param etaxConfigPath 电子税务局配置地址
*/
public Tianjin(RpaConfig rpaConfig, String etaxConfigPath) {
super(rpaConfig, etaxConfigPath);
if (!rpa().isCreate()) {
rpa().create();
}
}
/**
* 电子税务局登录
*
* @param taxFileNo 税号
* @param personId 办税人 ID
* @param personPassword 办税人密码
* @return 登录操作结果
*/
public FunctionResult<Object> login(String taxFileNo, String personId, String personPassword) {
// 1. 登录状态判断,
var sessionUser = this.function(SessionFunction.class).getSessionUser(true);
if (!Objects.isNull(sessionUser)) {
// 判断当前登录用户是否与要登录的用户一致
if (Objects.equals(sessionUser.userName(), personId)) {
if (Objects.equals(taxFileNo, sessionUser.taxFileNo())) {
return new FunctionResult<>(true, "登录成功。", null);
}
// 尝试切换企业
if (this.function(SwitchTaxpayerFunction.class).switchTaxpayer(taxFileNo).success()) {
return new FunctionResult<>(true, "登录成功。", null);
}
}
}
return this.function(LoginFunction.class).login(taxFileNo, personId, personPassword);
}
/**
* 查询自然人绑定企业信息
*
* @param personId 自然人账号
* @param personPassword 自然人密码
* @return 查询结果
*/
public FunctionResult<List<String>> queryEnterpriseInHuman(String personId, String personPassword) {
var loginResult = this.function(HumanLoginFunction.class).login(personId, personPassword);
if (!loginResult.success()) {
return new FunctionResult<>(false, loginResult.message(), null);
}
return this.function(QueryBindEnterpriseFunction.class).queryBindEnterprise();
}
/**
* 查询纳税人完税凭证
*
* @param taxFileNo 纳税人识别号
* @param personId 办税人账号
* @param personPassword 办税人密码
* @param taxpayerId 登记序号
* @param startDate 查询开始时间
* @param endDate 查询终止时间
* @return 提交到 OSS 中的文件编号
*/
public FunctionResult<String> queryDutyPaidProof(String taxFileNo, String personId, String personPassword, String taxpayerId, String startDate, String endDate) {
// 当前发票需要办税类角色
this.setRole(ROLE_TYPE_TAX);
// 1. 登录
var result = login(taxFileNo, personId, personPassword);
if (!result.success()) {
// 登录失败直接返回
return new FunctionResult<>(false, result.message(), null);
}
// 2. 查询完税证明,并返回结果
return this.function(QueryDutyPaidProofFunction.class).queryDutyPaidProof(taxpayerId, startDate, endDate);
}
}
package com.greatchn.etax.tianjin.bean;
/**
* 天津市电子税务局会话用户信息
*
* @param token 令牌
* @param ip IP
* @param loginType 登录类型
* @param newloginbs 首次登录表示
* @param gdbz 国地税标志
* @param ryyzlx 人员认证类型
* @param ryId 人员 ID
* @param frName 法人名称
* @param ryName 人员名称
* @param ryType 人员类型
* @param sjh 手机号
* @param yys 暂时未知
* @param ryTypeMc 人员类型名称
* @param ryZjLxDm 人员证件类型代码
* @param ryZjLxMc 人员证件类型名称
* @param ryZjhm 人员证件号码
* @param id 某种 ID,用途未知
* @param nsrmc 纳税人名称
* @param ykNsrmc 某种纳税人名称,用途未知
* @param ssdabh 税收档案编号
* @param djxh 登记序号
* @param nsrsbhG 国税:纳税人识别号
* @param djxhG 国税:登记序号
* @param shxydmG 国税:社会信用代码
* @param swjgDmG 国税:税务机构代码
* @param swjgMcG 国税:税务机构名称
* @param swksDmG 国税:税务科室代码
* @param swksMcG 国税:税务科室明晨
* @param swryDMG 国税:税务人员代码
* @param swryMcG 国税:税务人员名称
* @param nsrsbhD 地税:纳税人识别号
* @param djxhD 地税:登记序号
* @param shxydmD 地税:社会信用代码
* @param swjgDmD 地税:税务机构代码
* @param swjgMcD 地税:税务机构名称
* @param swksDmD 地税:税务科室代码
* @param swksMcD 地税:税务科室名称
* @param swryDMD 地税:税务人员代码
* @param swryMcD 地税:税务人员名称
* @param xydj 信用登记
* @param confirmStatus 信息确认状态
* @param clientId 客户端 ID
* @param userType 用户类型
* @param enterpriseType 企业类型
* @param initialEnterpriseType 企业初始状态
* @param userId 用户 ID
* @param enterpriseId 企业 ID
* @param agencyEnterpriseId 代理机构 ID
* @param userName 当前登录用户个性化账号
* @param trustedLevel 信用登记名称
* @param loginLevel 登录登记
* @param lastTrustedDate 信用登记更新时间
* @param lastFaceVerifyDate 人脸识别时间
* @param registerTime 登录用户注册时间
* @param taxFileNo 税号
* @param taxpayerStatusCode 纳税人状态代码
* @param areaPrefix 区域编号
* @param areaPreName 区域名称
* @param areaName 地区名称
* @param crossInspectionNumber 跨地区客户编号
* @param projectName 项目名称
* @param crossRegionalPropertyTaxSubjectRegistrationMark 跨区注册标志
* @param gender 性别
* @param nationality 国籍
* @param startDate 启用时间
* @param endDate 停用时间
* @param birthdate 出生日期
* @param address 联系地址
* @param email 联系电子邮箱
* @param enterpriseStatus 企业状态
* @param loginBz 登录标志
* @author LWang 2023.01.30
* @since 1.0.0
*/
public record SessionUser(
String token,
String ip,
String loginType,
String newloginbs,
String gdbz,
String ryyzlx,
String ryId,
String frName,
String ryName,
String ryType,
String sjh,
String yys,
String ryTypeMc,
String ryZjLxDm,
String ryZjLxMc,
String ryZjhm,
String id,
String nsrmc,
String ykNsrmc,
String ssdabh,
String djxh,
String nsrsbhG,
String djxhG,
String shxydmG,
String swjgDmG,
String swjgMcG,
String swksDmG,
String swksMcG,
String swryDMG,
String swryMcG,
String nsrsbhD,
String djxhD,
String shxydmD,
String swjgDmD,
String swjgMcD,
String swksDmD,
String swksMcD,
String swryDMD,
String swryMcD,
String xydj,
String confirmStatus,
String clientId,
String userType,
String enterpriseType,
String initialEnterpriseType,
String userId,
String enterpriseId,
String agencyEnterpriseId,
String userName,
String trustedLevel,
String loginLevel,
String lastTrustedDate,
String lastFaceVerifyDate,
String registerTime,
String taxFileNo,
String taxpayerStatusCode,
String areaPrefix,
String areaPreName,
String areaName,
String crossInspectionNumber,
String projectName,
String crossRegionalPropertyTaxSubjectRegistrationMark,
String gender,
String nationality,
String startDate,
String endDate,
String birthdate,
String address,
String email,
String enterpriseStatus,
String loginBz
) {
}
package com.greatchn.etax.tianjin.bean;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
/**
* 天津市电子税务局统一身份认证 Ajax 响应对象
*
* @author LWang 2023.02.01
* @since 1.0.0
*/
@Data
public class SysApiResponse {
/**
* 响应码
*/
private int code;
/**
* 响应消息
*/
private String msg;
/**
* 压缩码
*/
private String zipCode;
/**
* 加密码
*/
private String encryptCode;
/**
* 响应数据
*/
private String datagram;
/**
* 签名类型
*/
private String signtype;
/**
* 签名
*/
private String signature;
/**
* 响应时间戳
*/
private String timestamp;
protected static final int SUCCESS_CODE = 1000;
protected static final String DEFAULT_MESSAGE = "网络异常!";
public boolean success() {
return SUCCESS_CODE == this.getCode();
}
public String msg() {
return StringUtils.isBlank(this.getMsg()) ? DEFAULT_MESSAGE : this.getMsg();
}
}
package com.greatchn.kits;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
/**
* 日期时间工具
*
* @author LWang 2023.02.01
* @since 1.0.0
*/
public final class DateTimeKits {
private DateTimeKits() {
}
/**
* 计算今日剩余时间
*
* @param unit 时间单位,只支持 SECONDS,MINUTES,HOURS
* @return 剩余时间
*/
public static long timeRemaining(ChronoUnit unit) {
var now = LocalDateTime.now();
var tomorrow = now.plusDays(1).withHour(0).withMinute(0).withSecond(0);
var duration = Duration.between(now, tomorrow);
switch (unit) {
case SECONDS -> {
return duration.toSeconds();
}
case MINUTES -> {
return duration.toMinutes();
}
case HOURS -> {
return duration.toHours();
}
default -> throw new IllegalArgumentException("时间单位不符合预期!");
}
}
/**
* 获取当前日期(yyyy-MM-dd)格式
*
* @return 当前日期
*/
public static String currentDay() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.CHINA);
return formatter.format(LocalDate.now());
}
public static void main(String[] args) {
System.out.println(currentDay());
}
}
package com.greatchn.kits;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.management.ManagementFactory;
import java.net.Inet4Address;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
/**
* 系统环境信息处理工具
*
* @author LWang 2023.01.19
* @since 1.0.0
*/
@Slf4j
public final class EnvironmentToolkit {
private EnvironmentToolkit() {
}
/**
* 本机 IP 信息,防止重复获取
*/
private static volatile String LOCAL_IP;
private static volatile String MAC;
private static volatile List<Integer> LOCAL_IP_VALUES;
private static final String UNKNOWN = "unknown";
private static final String COMMA = ",";
private static synchronized void getIpInfos() {
if (StringUtils.isBlank(LOCAL_IP) || Objects.isNull(LOCAL_IP_VALUES) || LOCAL_IP_VALUES.isEmpty()) {
try {
final var ipSet = new HashSet<String>();
final var macSet = new HashSet<>();
LOCAL_IP_VALUES = new ArrayList<>();
NetworkInterface
.getNetworkInterfaces()
.asIterator()
.forEachRemaining(networkInterface -> {
try {
if (networkInterface.isUp() && !networkInterface.isLoopback() && !networkInterface.isVirtual()) {
networkInterface
.getInetAddresses()
.asIterator()
.forEachRemaining(inetAddress -> {
if (inetAddress instanceof Inet4Address) {
ipSet.add(inetAddress.getHostAddress());
byte[] ipAddress = inetAddress.getAddress();
LOCAL_IP_VALUES.add((ipAddress[0] << 24) & (ipAddress[1] << 16) & (ipAddress[2] << 8) & ipAddress[3]);
}
});
var hardwareAddress = networkInterface.getHardwareAddress();
if (ArrayUtils.isNotEmpty(hardwareAddress)) {
macSet.add(HashKits.toHex(hardwareAddress));
}
}
} catch (SocketException ex) {
log.error("获取本机 IP 地址发生异常:", ex);
}
});
LOCAL_IP = StringUtils.join(ipSet, COMMA);
MAC = StringUtils.join(macSet, COMMA);
} catch (SocketException e) {
log.error("获取本机 IP 地址发生异常:", e);
}
}
}
/**
* 获取本机 IP
* 返回结果忽略本地回环地址(127.0.0.1),localhost 和 IPv6 地址,如果是明确的虚拟网卡也忽略
*
* @return 本机 IP(多个 IP 使用 "," 连接)
*/
public static String getLocalIp() {
if (StringUtils.isBlank(LOCAL_IP)) {
getIpInfos();
}
return LOCAL_IP;
}
/**
* 获取本机 MAC 地址
*
* @return 本机 MAC(多个 MAC 使用 "," 连接)
*/
public static String getMac() {
if (StringUtils.isBlank(MAC)) {
getIpInfos();
}
return MAC;
}
/**
* 获取本机 IP 值
* 返回结果忽略本地回环地址(127.0.0.1),localhost 和 IPv6 地址,如果是明确的虚拟网卡也忽略
*
* @return 整型列表,IP 地址的 int 表示
*/
public static List<Integer> getLocalIpValue() {
if (Objects.isNull(LOCAL_IP_VALUES) || LOCAL_IP_VALUES.isEmpty()) {
getIpInfos();
}
return LOCAL_IP_VALUES;
}
/**
* 获取当前进程 ID
*
* @return 当前进程 ID
*/
public static long getProcessId() {
return ManagementFactory.getRuntimeMXBean().getPid();
}
}
package com.greatchn.kits;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
/**
* 哈希工具
*
* @author LWang 2023.01.19
* @since 1.0.0
*/
public final class HashKits {
private HashKits() {
}
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
/**
* 将字节数组转换为 Hex
*
* @param bytes 要转换的字节数据
* @return 转换后的 HEX 字符串
*/
public static String toHex(byte[] bytes) {
return toHex(bytes, false);
}
/**
* 将字节数组转换为 Hex
*
* @param bytes 要转换的字节数据
* @param upperCase 结果转换为大写字母
* @return 转换后的 HEX 字符串
*/
public static String toHex(byte[] bytes, boolean upperCase) {
if (ArrayUtils.isEmpty(bytes)) {
return StringUtils.EMPTY;
}
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(HEX_DIGITS[0x0F & (b >> 4)]);
result.append(HEX_DIGITS[0x0F & b]);
}
return upperCase ? result.toString().toUpperCase() : result.toString().toLowerCase();
}
/**
* 将 Hex 字符串反编码为 byte 数组
*
* @param hexStr 要反编码的字符串
* @return 反编码结果
*/
public static byte[] decodeHex(String hexStr) {
if (StringUtils.isBlank(hexStr)) {
throw new IllegalArgumentException("反编码 HEX 信息错误,目标字符串为空");
}
return decodeHex(hexStr.toCharArray());
}
/**
* 将 Hex 字符数组反编码为 byte 数组
*
* @param chars 要反编码的字符数组
* @return 反编码结果
*/
public static byte[] decodeHex(char[] chars) {
if (ArrayUtils.isEmpty(chars)) {
throw new IllegalArgumentException("反编码 HEX 信息错误,目标字符数组为空!");
}
if (chars.length % 2 != 0) {
throw new IllegalArgumentException("反编码 HEX 信息错误,目标字符数组长度不是偶数!");
}
ByteBuffer byteBuffer = ByteBuffer.allocate(chars.length / 2);
for (int i = 0; i < chars.length; i += 2) {
byteBuffer.put((byte) ((toDigit(chars[i]) << 4) | toDigit(chars[i + 1])));
}
return byteBuffer.array();
}
private static int toDigit(char c) {
int digit = Character.digit(c, 16);
if (digit < 0) {
throw new IllegalArgumentException(String.format("无法将字符 %c 转换为十六进制数字!", c));
}
return digit;
}
/**
* 将字节数组转换为 MD5 摘要
*
* @param bytes 要转换的字节数组
* @return 转换结果,HEX 字符串
*/
public static String md5(byte[] bytes) {
return md5(bytes, false);
}
/**
* 将字节数组转换为 MD5 摘要
*
* @param bytes 要转换的字节数组
* @param upperCase 结果是否大写
* @return 转换结果,HEX 字符串
*/
public static String md5(byte[] bytes, boolean upperCase) {
return hash("MD5", bytes, upperCase);
}
/**
* 将字节数组转换为 SHA1 摘要
*
* @param bytes 要转换的字节数组
* @return 转换结果,HEX 字符串
*/
public static String sha1(byte[] bytes) {
return sha1(bytes, false);
}
/**
* 将字节数组转换为 SHA1 摘要
*
* @param bytes 要转换的字节数组
* @param upperCase 结果是否大写
* @return 转换结果,HEX 字符串
*/
public static String sha1(byte[] bytes, boolean upperCase) {
return hash("SHA-1", bytes, upperCase);
}
/**
* 将字节数组转换为 SHA256 摘要
*
* @param bytes 要转换的字节数组
* @return 转换结果,HEX 字符串
*/
public static String sha256(byte[] bytes) {
return sha256(bytes, false);
}
/**
* 将字节数组转换为 SHA256 摘要
*
* @param bytes 要转换的字节数组
* @param upperCase 结果是否大写
* @return 转换结果,HEX 字符串
*/
public static String sha256(byte[] bytes, boolean upperCase) {
return hash("SHA-256", bytes, upperCase);
}
/**
* 将字节数组转换为 SHA512 摘要
*
* @param bytes 要转换的字节数组
* @return 转换结果,HEX 字符串
*/
public static String sha512(byte[] bytes) {
return sha512(bytes, false);
}
/**
* 将字节数组转换为 SHA512 摘要
*
* @param bytes 要转换的字节数组
* @param upperCase 结果是否大写
* @return 转换结果,HEX 字符串
*/
public static String sha512(byte[] bytes, boolean upperCase) {
return hash("SHA-512", bytes, upperCase);
}
/**
* 生成基于 SHA 256 摘要算法的签名数据,结果使用 HEX 方式编码
*
* @param parameters 要进行签名的数据
* @param secretKey 安全密钥键,如果为空,表示不设置安全密钥
* @param secretValue 安全密钥值,如果 secretKey 不为空,则 secretValue 必须存在
* @return 签名结果
*/
public static String signatureWithSha256(final Map<String, String> parameters, String secretKey, String secretValue) {
if (Objects.isNull(parameters)) {
throw new IllegalArgumentException("签名数据不能为空!");
}
if (StringUtils.isNotBlank(secretKey) && StringUtils.isBlank(secretValue)) {
throw new IllegalArgumentException("安全密钥值不能为空!");
}
var sortMap = new TreeMap<>(parameters);
if (StringUtils.isNotBlank(secretKey)) {
sortMap.put(secretKey, secretValue);
}
var data = StringUtils.join(sortMap.values()).getBytes(StandardCharsets.UTF_8);
return sha256(data);
}
/**
* 对基于 SHA 256 摘要算法生成的签名进行验签
*
* @param parameters 待验签的数据
* @param signature 签名数据
* @param secretKey 安全密钥键,如果为空,表示不设置安全密钥
* @param secretValue 安全密钥值,如果 secretKey 不为空,则 secretValue 必须存在
* @return 验签结果
*/
public static boolean verifyWithSha256(final Map<String, String> parameters, String signature, String secretKey, String secretValue) {
return Objects.equals(
signatureWithSha256(parameters, secretKey, secretValue),
signature
);
}
/**
* 进行摘要计算
*
* @param algorithm 算法
* @param bytes 要进行摘要计算的字节数组
* @param upperCase 结果是否大写
* @return 转换结果,HEX 字符串
*/
private static String hash(String algorithm, byte[] bytes, boolean upperCase) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
return toHex(md.digest(bytes), upperCase);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
This diff is collapsed.
package com.greatchn.kits;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.greatchn.kits.exceptions.PackingException;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.TimeZone;
/**
* Jackson 序列化反序列化工具
*
* @author LWang 2023.01.18
* @since 1.0.0
*/
public final class JacksonKits {
private JacksonKits() {
}
private static volatile ObjectMapper OBJECT_MAPPER;
/**
* 将对象转换为 Json 字符串
*
* @param source 要执行转换的对象
* @return 序列化后的 Json 字符串
*/
public static String toJson(Object source) {
if (null == source) {
// null 对象,返回空的 json 对象
return "{}";
}
try {
return getObjectMapper().writeValueAsString(source);
} catch (JsonProcessingException e) {
throw new PackingException(e);
}
}
/**
* 将 Json 字符串转为对象
*
* @param content 要执行反序列化的 Json 字符串
* @param cls 序列化结果类
* @param <T> 结果类泛型
* @return 反序列化结果
*/
public static <T> T toBean(String content, Class<T> cls) {
if (StringUtils.isBlank(content) || cls == null) {
return null;
}
ObjectMapper objectMapper = getObjectMapper();
try {
return objectMapper.readValue(content, cls);
} catch (JsonProcessingException e) {
throw new PackingException(e);
}
}
/**
* @param content 要执行反序列化的 Json 字符串
* @param ref 序列化结果类型
* @param <T> 结果类泛型
* @return 反序列化结果
*/
public static <T> T toBean(String content, TypeReference<T> ref) {
if (StringUtils.isBlank(content) || ref == null) {
return null;
}
ObjectMapper objectMapper = getObjectMapper();
try {
return objectMapper.readValue(content, ref);
} catch (JsonProcessingException e) {
throw new PackingException(e);
}
}
/**
* 将 Json 字符串转为对象列表
*
* @param content 要执行反序列化的 Json 字符串
* @param ref 序列化结果类型
* @param <T> 结果类泛型
* @return 反序列化结果
*/
public static <T> List<T> toList(String content, TypeReference<List<T>> ref) {
if (StringUtils.isBlank(content) || ref == null) {
return null;
}
ObjectMapper objectMapper = getObjectMapper();
try {
return objectMapper.readValue(content, ref);
} catch (JsonProcessingException e) {
throw new PackingException(e);
}
}
/**
* 将 Json 字符串转为 JsonNode 对象,适用于处理无需建立 JavaBean 的临时 Json 字符串处理
*
* @param content 要执行反序列化的 Json 字符串
* @return 反序列化结果
*/
public static JsonNode toJsonNode(String content) {
try {
return getObjectMapper().readTree(content);
} catch (JsonProcessingException e) {
throw new PackingException(e);
}
}
/**
* 获取一个 ObjectMapper 对象
*
* @return ObjectMapper 对象
*/
public static ObjectMapper getObjectMapper() {
if (OBJECT_MAPPER == null) {
synchronized (JacksonKits.class) {
if (OBJECT_MAPPER == null) {
ObjectMapper objectMapper = new ObjectMapper();
// 设置时区
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
// 设置时间格式
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
JavaTimeModule javaTimeModule = new JavaTimeModule();
/*
* 序列化配置,针对java8 时间
*/
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
/*
* 反序列化配置,针对java8 时间
*/
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
/*
* 声明自定义模块,配置double类型序列化配置
*/
SimpleModule module = new SimpleModule("DoubleSerializer", new Version(1, 0, 0, "", "", ""));
// 注意Double和double需要分配配置
JsonSerializer<Double> serializer = new JsonSerializer<>() {
@Override
public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
BigDecimal d = BigDecimal.valueOf(value);
gen.writeNumber(d.stripTrailingZeros().toPlainString());
}
@Override
public Class<Double> handledType() {
return Double.class;
}
};
module.addSerializer(Double.class, serializer);
module.addSerializer(double.class, serializer);
/*
* 注册模块
*/
objectMapper
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(javaTimeModule)
.registerModule(module)
.registerModule(new Jdk8Module())
.registerModule(new ParameterNamesModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
OBJECT_MAPPER = objectMapper;
}
}
}
return OBJECT_MAPPER;
}
}
package com.greatchn.kits;
import java.security.SecureRandom;
/**
* 随机数工具,提供生成随机数字和随机字符串功能
* <p>
* 本工具类中的方法都可以使用 Apace Common-lang3 中的工具替代,不过在简单环境下,建议使用本工具类
*
* @author LWang 2023.01.20
* @see org.apache.commons.lang3.RandomUtils
* @see org.apache.commons.lang3.RandomStringUtils
* @since 1.0.0
*/
public final class RandomKits {
private RandomKits() {
}
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final char[] CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*-_=+".toCharArray();
/**
* 获取一个随机整数
*
* @return 随机数
*/
public static int nextInt() {
return SECURE_RANDOM.nextInt();
}
/**
* 获取一个随机整数
*
* @param bound 随机数范围上限
* @return 随机数
*/
public static int nextInt(int bound) {
return SECURE_RANDOM.nextInt(bound);
}
/**
* 获取一个随机整数
*
* @param origin 随机数范围下限
* @param bound 随机数范围上限
* @return 随机数
*/
public static int nextInt(int origin, int bound) {
return SECURE_RANDOM.nextInt(origin, bound);
}
/**
* 获取一个随机长整数
*
* @return 随机数
*/
public static long nextLong() {
return SECURE_RANDOM.nextLong();
}
/**
* 获取一个随机长整数
*
* @param bound 随机数范围上限
* @return 随机数
*/
public static long nextLong(long bound) {
return SECURE_RANDOM.nextLong(bound);
}
/**
* 获取一个随机长整数
*
* @param origin 随机数范围下限
* @param bound 随机数范围上限
* @return 随机数
*/
public static long nextLong(long origin, long bound) {
return SECURE_RANDOM.nextLong(origin, bound);
}
/**
* 获取一个随机单精浮点数
*
* @return 随机数
*/
public static float nextFloat() {
return SECURE_RANDOM.nextFloat();
}
/**
* 获取一个随机单精浮点数
*
* @param bound 随机数范围上限
* @return 随机数
*/
public static float nextFloat(float bound) {
return SECURE_RANDOM.nextFloat(bound);
}
/**
* 获取一个随机单精浮点数
*
* @param origin 随机数范围下限
* @param bound 随机数范围上限
* @return 随机数
*/
public static float nextFloat(float origin, float bound) {
return SECURE_RANDOM.nextFloat(origin, bound);
}
/**
* 获取一个随机双精浮点数
*
* @return 随机数
*/
public static double nextDouble() {
return SECURE_RANDOM.nextDouble();
}
/**
* 获取一个随机双精浮点数
*
* @param bound 随机数范围上限
* @return 随机数
*/
public static double nextDouble(double bound) {
return SECURE_RANDOM.nextDouble(bound);
}
/**
* 获取一个随机双精浮点数
*
* @param origin 随机数范围下限
* @param bound 随机数范围上限
* @return 随机数
*/
public static double nextDouble(double origin, double bound) {
return SECURE_RANDOM.nextDouble(origin, bound);
}
/**
* 生成一个随机字符串,使用数字、字母和常用符号组成,也可以使用 Apache Common-lang3 中的 RandomStringUtils 类中的对应方法,不过那个考虑的情况太多,
* 在使用要求不复杂的情况下,使用本方法即可
*
* @param length 随机字符串长度
* @return 随机字符串
* @see org.apache.commons.lang3.RandomStringUtils
*/
public static String nextString(final int length) {
if (length <= 0) {
throw new IllegalArgumentException("生成随机字符串长度必须大于零!");
}
StringBuilder builder = new StringBuilder(length);
int bound = CHARS.length;
int count = length;
while (count-- != 0) {
builder.append(CHARS[nextInt(bound)]);
}
return builder.toString();
}
}
package com.greatchn.kits;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
/**
* Yaml 配置文件加载工具
*
* @author LWang 2023.01.29
* @since 1.0.0
*/
public final class YamlKits {
private YamlKits() {
}
/**
* 读取 YAML 配置文件
*
* @param config 配置文件路径
* @param clazz 配置对象映射类
* @param <T> 配置对象泛型
* @return 配置对象
*/
public static <T> T load(String config, Class<T> clazz) {
try (var in = ClassLoader.getSystemResourceAsStream(config)) {
return new Yaml(new Constructor(clazz)).load(in);
} catch (Exception ex) {
throw new IllegalArgumentException("指定的配置文件无效!", ex);
}
}
}
package com.greatchn.kits.exceptions;
import lombok.Getter;
/**
* 异常封装类,业务中的异常可以直接封装为此类对外抛出,在使用的地方在提取实际异常进行异常记录或处理,此异常不包含实际业务意义
*
* @author LWang 2023.01.18
* @since 1.0.0
*/
public class PackingException extends RuntimeException {
/**
* 实际的异常
*/
@Getter
private final Throwable rawCause;
public PackingException(Throwable cause) {
super(cause);
this.rawCause = cause;
}
}
package com.greatchn.kits.oss;
import com.greatchn.kits.HttpTool;
import com.greatchn.kits.JacksonKits;
import com.greatchn.kits.YamlKits;
import lombok.Data;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpStatus;
import java.io.File;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
/**
* Oss 工具
*
* @author LWang 2023.02.02
* @since 1.0.0
*/
public final class OssKits {
private OssKits() {
}
@Data
public static class OssConfig {
private String url;
}
@Data
public static class OssResult {
private int code;
private String data;
private String msg;
private String traceId;
}
private final static HttpTool HTTP_TOOL;
private final static OssConfig CONFIG;
static {
try {
HTTP_TOOL = new HttpTool();
CONFIG = YamlKits.load("oss.yaml", OssConfig.class);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
/**
* 将文件上传到 OSS
*
* @param file 要上传的文件
* @return OSS 资源 ID
*/
public static String upload(File file) throws IOException, InterruptedException {
var url = "%s/res/support/api/oss/upload".formatted(CONFIG.url);
var result = HTTP_TOOL.post(url, null, null, new ArrayList<>() {{
this.add(Pair.of("file", file));
}}, null, null);
if (!result.getKey().equals(HttpStatus.SC_OK)) {
throw new RuntimeException("上传资源文件异常,服务无响应。");
}
var ossResult = JacksonKits.toBean(result.getValue(), OssResult.class);
assert ossResult != null;
if (ossResult.getCode() != HttpStatus.SC_OK) {
throw new RuntimeException("上传资源文件异常," + ossResult.getMsg() + "," + ossResult.getTraceId());
}
return ossResult.data;
}
public static void main(String[] args) throws IOException, InterruptedException {
upload(new File("d:/result.jpg"));
}
}
This diff is collapsed.
package com.greatchn.rpa;
import com.greatchn.rpa.beans.Button;
import com.greatchn.rpa.beans.Point;
import java.awt.*;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Windows Api 封装
*
* @author LWang 2023.01.28
* @since 1.0.0
*/
public final class WinApi {
private WinApi() {
}
private static final Robot ROBOT;
static {
try {
ROBOT = new Robot();
} catch (AWTException e) {
throw new RuntimeException(e);
}
}
private static final int MINIMUM_DURATION = 100;
private static final int MINIMUM_SLEEP = 50;
public static void mouseMove(double x, double y) {
ROBOT.mouseMove((int) x, (int) y);
}
/**
* 将鼠标指针移动到指定坐标
*
* @param x 横轴
* @param y 纵轴
* @param duration 延时
*/
public static void mouseMove(double x, double y, long duration) {
// 切分坐标
// 读取当前坐标点
var currentPoint = currentPoint();
// 计算移动长宽
var width = x - currentPoint.x();
var height = y - currentPoint.y();
// 每一步移动点
var points = new ArrayList<Point>();
var sleepAmount = 0;
// 最大移动距离(绝对值比较)
var numSteps = (int) (Math.max(Math.abs(width), Math.abs(height)));
if (numSteps > 0) {
// 每一步移动间隔
sleepAmount = (int) (duration / numSteps);
if (duration > MINIMUM_DURATION) {
if (sleepAmount < MINIMUM_DURATION) {
numSteps = (int) (duration / MINIMUM_SLEEP);
sleepAmount = (int) (duration / numSteps);
}
if (numSteps > 0) {
// 计算每一步移动点
var widthSection = width / numSteps;
var heightSection = height / numSteps;
for (var i = 0; i < numSteps; i++) {
currentPoint = new Point(currentPoint.x() + widthSection, currentPoint.y() + heightSection);
points.add(currentPoint);
}
}
}
}
points.add(new Point(x, y));
for (var p : points) {
mouseMove(p.x(), p.y());
ROBOT.delay(sleepAmount);
}
}
/**
* 按下鼠标按键
*
* @param button 按键
*/
public static void mouseDown(Button button) {
ROBOT.mousePress(button.getButton());
}
/**
* 抬起鼠标按键
*
* @param button 按键
*/
public static void mouseUp(Button button) {
ROBOT.mouseRelease(button.getButton());
}
/**
* 鼠标左键点击
*/
public static void leftClick() {
mouseDown(Button.LEFT);
ROBOT.delay(100);
mouseUp(Button.LEFT);
}
/**
* 获取当前鼠标指定定位
*
* @return 当前鼠标定位
*/
public static Point currentPoint() {
var pointInfo = MouseInfo.getPointerInfo().getLocation();
return new Point(pointInfo.getX(), pointInfo.getY());
}
/**
* 拖拽到指定位置
*
* @param x 横坐标
* @param y 纵坐标
* @param duration 延时
*/
public static void dragTo(double x, double y, long duration) {
mouseDown(Button.LEFT);
ROBOT.delay(100);
mouseMove(x, y, duration);
mouseUp(Button.LEFT);
}
/**
* 延时
*
* @param delay 延时(毫秒)
*/
public static void delay(long delay) {
try {
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException ignored) {
}
}
public static void main(String[] args) {
mouseMove(0, 0);
mouseMove(1000, 1000, 1000);
}
}
package com.greatchn.rpa.beans;
import java.awt.event.InputEvent;
/**
* 鼠标按键枚举
*
* @author LWang 2023.01.28
* @since 1.0.0
*/
public enum Button {
/**
* 左键
*/
LEFT(InputEvent.BUTTON1_DOWN_MASK),
/**
* 中键
*/
MIDDLE(InputEvent.BUTTON2_DOWN_MASK),
/**
* 右键
*/
RIGHT(InputEvent.BUTTON3_DOWN_MASK);
final int button;
public int getButton() {
return button;
}
Button(int button) {
this.button = button;
}
}
package com.greatchn.rpa.beans;
/**
* 点对象
*
* @param x 横坐标
* @param y 纵坐标
* @author LWang 2023.01.28
* @since 1.0.0
*/
public record Point(double x, double y) {
}
package com.greatchn.rpa.beans;
/**
* 矩形对象
*
* @param leftTop 矩形左上角坐标点
* @param rightTop 矩形右上角坐标点
* @param rightBottom 矩形右下角坐标点
* @param leftBottom 矩形左下角坐标点
* @author LWang 2023.01.28
* @since 1.0.0
*/
public record Rectangle(
Point leftTop,
Point rightTop,
Point rightBottom,
Point leftBottom
) {
}
package com.greatchn.rpa.config;
import com.greatchn.kits.YamlKits;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.io.File;
import java.net.URISyntaxException;
/**
* 机器人配置对象
*
* @author LWang 2023.01.28
* @since 1.0.0
*/
@Data
@ToString
@NoArgsConstructor
public class RpaConfig {
private Selenium selenium;
private Browser browser;
private Rpa rpa;
private Double scale;
/**
* 使用默认配置文件加载配置
*
* @return 配置对象
*/
public static RpaConfig build() {
return build(DEFAULT_CONFIG);
}
/**
* 创建配置对象
*
* @param config 配置文件地址
* @return 配置对象
*/
public static RpaConfig build(String config) {
return YamlKits.load(config, RpaConfig.class);
}
/**
* 默认配置文件
*/
private static final String DEFAULT_CONFIG = "rpa.yaml";
/**
* 默认配置:selenium 默认驱动
*/
private static final String DEFAULT_DRIVER = "driver/chromedriver109.exe";
/**
* 默认配置:CHROME 启动路径
*/
private static final String DEFAULT_CHROME_BIN = null;
/**
* 默认配置:是否开启代理
*/
private static final boolean DEFAULT_ENABLE_PROXY = true;
/**
* 默认配置:代理端口
*/
private static final int DEFAULT_PROXY_PORT = 9099;
/**
* 默认配置:初始化自动清理 Cookies
*/
private static final boolean DEFAULT_CLEAN_COOKIES = true;
/**
* 默认配置:浏览器全屏展示(F11)
*/
private static final boolean DEFAULT_FULL_SCREEN = false;
/**
* 默认配置:交互时,默认滚动到元素
*/
private static final boolean DEFAULT_AUTO_SCROLL = true;
/**
* 默认配置:构造对象是自动创建自动化浏览器实例
*/
private static final boolean DEFAULT_AUTO_CREATE = false;
/**
* 默认配置:模拟鼠标操作
*/
private static final boolean DEFAULT_SIMULATE = true;
@Data
@ToString
@NoArgsConstructor
public static class Selenium {
/**
* Selenium 驱动地址
*/
private String driver = DEFAULT_DRIVER;
public File getExe() {
try {
var exe = new File(ClassLoader.getSystemResource(driver).toURI());
if (!exe.exists() || !exe.isFile()) {
throw new IllegalArgumentException("Selenium 驱动不可用!");
}
return exe;
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Selenium 驱动不可用!");
}
}
}
@Data
@ToString
@NoArgsConstructor
public static class Browser {
/**
* Chrome 启动路径
*/
private String chrome = DEFAULT_CHROME_BIN;
}
@Data
@ToString
@NoArgsConstructor
public static class Rpa {
private Proxy proxy;
private Cookies cookies;
/**
* Selenium 创建时,是否全屏浏览器
*/
private boolean fullScreen = DEFAULT_FULL_SCREEN;
/**
* Selenium 交互元素时,是否自动滚动到元素可见位置
*/
private boolean autoScroll = DEFAULT_AUTO_SCROLL;
/**
* 构造 RPA 对象时,是否自动创建 Selenium 对象
*/
private boolean autoCreate = DEFAULT_AUTO_CREATE;
/**
* 是否启用基于 Windows API 的模拟交互操作
*/
private boolean simulate = DEFAULT_SIMULATE;
}
@Data
@ToString
@NoArgsConstructor
public static class Proxy {
/**
* 是否开启网络代理
*/
private boolean enable = DEFAULT_ENABLE_PROXY;
/**
* 网络代理端口
*/
private int port = DEFAULT_PROXY_PORT;
}
@Data
@ToString
@NoArgsConstructor
public static class Cookies {
/**
* 构造 Selenium 对象时,自动清理 cookies
*/
private boolean clean = DEFAULT_CLEAN_COOKIES;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
url: http://39.105.29.63
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment