├── doc ├── ddns4j-详细参数说明.png ├── ddns4j-web端-功能区介绍.png ├── ddns4j-移动端-功能区介绍.png ├── ddns4j交流群(一)群二维码.png ├── windows-scripts │ └── ddns4j_setup_package.iss ├── Dockerfile-v1.6.5-RELEASE │ └── Dockerfile └── linux-scripts │ └── ddns4j.sh ├── src └── main │ ├── resources │ ├── mapper │ │ ├── JobTaskMapper.xml │ │ ├── ChangedLogMapper.xml │ │ └── ParsingRecordMapper.xml │ ├── application.yml │ ├── banner.txt │ ├── application-mysql.yml │ ├── application-h2.yml │ └── sql │ │ ├── ddns4j_h2.sql │ │ └── ddns4j_mysql.sql │ └── java │ └── top │ └── sssd │ └── ddns │ ├── common │ ├── constant │ │ ├── ExceptionConstant.java │ │ └── DDNSConstant.java │ ├── valid │ │ └── ValidGroup.java │ ├── BizException.java │ ├── ResultCodeEnum.java │ ├── enums │ │ ├── ServiceProviderEnum.java │ │ ├── RecordTypeEnum.java │ │ └── UpdateFrequencyEnum.java │ ├── utils │ │ ├── AmisPageUtils.java │ │ ├── DoMainUtil.java │ │ └── PageUtils.java │ ├── Result.java │ └── AmisResult.java │ ├── mapper │ ├── JobTaskMapper.java │ ├── ChangedLogMapper.java │ └── ParsingRecordMapper.java │ ├── service │ ├── ChangedLogService.java │ ├── NetWorkService.java │ ├── impl │ │ ├── ChangedLogServiceImpl.java │ │ ├── NetWorkServiceImpl.java │ │ ├── JobTaskServiceImpl.java │ │ └── ParsingRecordServiceImpl.java │ ├── IParsingRecordService.java │ └── IJobTaskService.java │ ├── model │ ├── response │ │ └── NetWorkSelectResponse.java │ └── entity │ │ ├── PageEntity.java │ │ ├── AmisPageEntity.java │ │ ├── ChangedLog.java │ │ ├── JobTask.java │ │ └── ParsingRecord.java │ ├── DynamicDnsApplication.java │ ├── config │ ├── IdentifierGeneratorConfig.java │ ├── DnsServiceTypeConfig.java │ ├── WebConfig.java │ ├── ApplicationContextProvider.java │ ├── RestTemplateConfig.java │ ├── StartupRunner.java │ ├── MybatisPlusConfig.java │ ├── H2Initializer.java │ └── MySQLInitializer.java │ ├── handler │ ├── EasySqlInjector.java │ ├── MyMetaObjectHandler.java │ └── GlobalExceptionHandler.java │ ├── strategy │ ├── DynamicDnsServiceFactory.java │ ├── DynamicDnsStrategy.java │ ├── HuaweiDynamicDnsStrategyImpl.java │ ├── CloudflareDynamicDnsStrategyImpl.java │ ├── AliDynamicDnsStrategyImpl.java │ └── TencentDynamicDnsStrategyImpl.java │ ├── controller │ ├── PublicAccessController.java │ ├── ChangedLogController.java │ └── ParsingRecordController.java │ ├── task │ ├── ClearLogJob.java │ └── DynamicDnsJob.java │ ├── interceptor │ └── ExcludeIndexPageInterceptor.java │ └── utils │ ├── TencentCloudAPITC3Singer.java │ ├── HuaweiDnsUtils.java │ ├── AliDnsUtils.java │ ├── CloudflareUtils.java │ └── TencentDnsUtils.java ├── .workflow ├── pr-pipeline.yml ├── master-pipeline.yml └── branch-pipeline.yml ├── README.md ├── pom.xml └── LICENSE /doc/ddns4j-详细参数说明.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sssdgithub/ddns4j/HEAD/doc/ddns4j-详细参数说明.png -------------------------------------------------------------------------------- /doc/ddns4j-web端-功能区介绍.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sssdgithub/ddns4j/HEAD/doc/ddns4j-web端-功能区介绍.png -------------------------------------------------------------------------------- /doc/ddns4j-移动端-功能区介绍.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sssdgithub/ddns4j/HEAD/doc/ddns4j-移动端-功能区介绍.png -------------------------------------------------------------------------------- /doc/ddns4j交流群(一)群二维码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sssdgithub/ddns4j/HEAD/doc/ddns4j交流群(一)群二维码.png -------------------------------------------------------------------------------- /doc/windows-scripts/ddns4j_setup_package.iss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sssdgithub/ddns4j/HEAD/doc/windows-scripts/ddns4j_setup_package.iss -------------------------------------------------------------------------------- /src/main/resources/mapper/JobTaskMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/ChangedLogMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/ParsingRecordMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/constant/ExceptionConstant.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.constant; 2 | 3 | /** 4 | * @author sssd 5 | * @created 2023-09-22-18:50 6 | */ 7 | public class ExceptionConstant { 8 | private ExceptionConstant() { 9 | } 10 | 11 | public static final String RECORD_EXISTS = "exists"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/mapper/JobTaskMapper.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import top.sssd.ddns.model.entity.JobTask; 5 | 6 | /** 7 | * @author sssd 8 | * @created 2023-05-02-11:02 9 | */ 10 | public interface JobTaskMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/mapper/ChangedLogMapper.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import top.sssd.ddns.model.entity.ChangedLog; 5 | 6 | /** 7 | * @author sssd 8 | * @careate 2023-10-31-16:47 9 | */ 10 | public interface ChangedLogMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/ChangedLogService.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import top.sssd.ddns.model.entity.ChangedLog; 5 | 6 | /** 7 | * @author sssd 8 | * @careate 2023-10-31-16:44 9 | */ 10 | public interface ChangedLogService extends IService { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/valid/ValidGroup.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.valid; 2 | 3 | import javax.validation.groups.Default; 4 | 5 | /** 6 | * @author sssd 7 | */ 8 | public class ValidGroup { 9 | public interface SaveGroup extends Default{} 10 | public interface UpdateGroup extends Default{} 11 | public interface CopyGroup extends Default{} 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/BizException.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author sssd 7 | */ 8 | @Getter 9 | public class BizException extends RuntimeException{ 10 | private final String message; 11 | public BizException(String message) { 12 | super(message); 13 | this.message = message; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/mapper/ParsingRecordMapper.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import top.sssd.ddns.model.entity.ParsingRecord; 5 | 6 | /** 7 | *

8 | * 解析记录表 Mapper 接口 9 | *

10 | * 11 | * @author sssd 12 | * @since 2023-03-19 13 | */ 14 | public interface ParsingRecordMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/response/NetWorkSelectResponse.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.response; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 返回前端下拉框模型 7 | * @author sssd 8 | * @created 2023-09-22-15:56 9 | */ 10 | @Data 11 | public class NetWorkSelectResponse { 12 | /** 13 | * 下拉框显示的网卡名及ip地址 14 | */ 15 | private String label; 16 | /** 17 | * 真正的ip地址 18 | */ 19 | private String value; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 10000 3 | spring: 4 | profiles: 5 | active: h2 # h2,mysql 6 | jackson: 7 | date-format: yyyy-MM-dd HH:mm:ss 8 | time-zone: GMT+8 9 | #日志目录 10 | logging: 11 | file: 12 | path: ./ 13 | name: ddns4j.log 14 | 15 | dns: 16 | serviceTypes: 17 | 1: aliDynamicDnsStrategyImpl 18 | 2: tencentDynamicDnsStrategyImpl 19 | 3: cloudflareDynamicDnsStrategyImpl 20 | 4: huaweiDynamicDnsStrategyImpl 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ██ ██ 2 | ░██ ██████ ░██ 3 | ██████ ██████ ░██░░░██ ██████ ██████ ██████ ░██ 4 | ░░░██░ ██░░░░██░██ ░██ ██░░░░ ██░░░░ ██░░░░ ██████ 5 | ░██ ░██ ░██░██████ ░░█████ ░░█████ ░░█████ ██░░░██ 6 | ░██ ░██ ░██░██░░░ ██ ░░░░░██ ░░░░░██ ░░░░░██░██ ░██ 7 | ░░██ ░░██████ ░██ ░██ ██████ ██████ ██████ ░░██████ 8 | ░░ ░░░░░░ ░░ ░░ ░░░░░░ ░░░░░░ ░░░░░░ ░░░░░░ 9 | :: v1.6-RELEASE :: (DDNS-Java) 10 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/DynamicDnsApplication.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * @author sssd 9 | * @created 2023-03-19-1:24 10 | */ 11 | @MapperScan("top.sssd.ddns.mapper") 12 | @SpringBootApplication 13 | public class DynamicDnsApplication { 14 | public static void main(String[] args) { 15 | SpringApplication.run(DynamicDnsApplication.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/NetWorkService.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service; 2 | 3 | import top.sssd.ddns.model.response.NetWorkSelectResponse; 4 | 5 | import java.net.SocketException; 6 | import java.util.List; 7 | 8 | /** 9 | * @author sssd 10 | * @created 2023-09-22-13:56 11 | */ 12 | public interface NetWorkService { 13 | 14 | /** 15 | * 获取网卡信息 16 | * @param recordType 17 | * @return 返回有效ip数组 18 | * @throws SocketException 19 | */ 20 | List networks(Integer recordType) throws SocketException; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/IdentifierGeneratorConfig.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author sssd 9 | * @created 2024-01-08-19:14 10 | */ 11 | @Configuration 12 | public class IdentifierGeneratorConfig { 13 | 14 | 15 | @Bean 16 | public DefaultIdentifierGenerator build(){ 17 | return new DefaultIdentifierGenerator(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/DnsServiceTypeConfig.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * @author sssd 12 | * @created 2024-05-08-10:58 13 | */ 14 | @Getter 15 | @Setter 16 | @Configuration 17 | @ConfigurationProperties(prefix = "dns") 18 | public class DnsServiceTypeConfig { 19 | 20 | private Map serviceTypes; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/impl/ChangedLogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import org.springframework.stereotype.Service; 5 | import top.sssd.ddns.mapper.ChangedLogMapper; 6 | import top.sssd.ddns.model.entity.ChangedLog; 7 | import top.sssd.ddns.service.ChangedLogService; 8 | 9 | /** 10 | * @author sssd 11 | * @careate 2023-10-31-16:44 12 | */ 13 | @Service 14 | public class ChangedLogServiceImpl extends ServiceImpl implements ChangedLogService { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/entity/PageEntity.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @author sssd 11 | * @created 2023-04-18-15:13 12 | */ 13 | @Data 14 | public class PageEntity implements Serializable { 15 | @TableField(exist = false) 16 | @JsonProperty("page") 17 | private Long page ; 18 | @TableField(exist = false) 19 | @JsonProperty("pageSize") 20 | private Long pageSize ; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/entity/AmisPageEntity.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @author sssd 11 | * @careate 2023-10-26-15:40 12 | */ 13 | @Data 14 | public class AmisPageEntity implements Serializable { 15 | @TableField(exist = false) 16 | @JsonProperty("page") 17 | private Long page ; 18 | 19 | @TableField(exist = false) 20 | @JsonProperty("perPage") 21 | private Long perPage ; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /doc/Dockerfile-v1.6.5-RELEASE/Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用alpine作为基础镜像 2 | FROM alpine:latest 3 | 4 | # 指定作者信息 5 | LABEL maintainer="15829762162@163.com" 6 | 7 | # 设置工作目录 8 | WORKDIR /usr/local/ 9 | 10 | # 暴露端口 11 | EXPOSE 10000 12 | 13 | # 使用openjdk 11作为基础镜像 14 | FROM openjdk:11-jdk-slim 15 | 16 | # 设置工作目录 17 | WORKDIR /usr/local/ 18 | 19 | # 将您的Web JAR文件复制到容器中的指定位置 20 | COPY ddns-v1.6.5-RELEASE.jar /usr/local/ddns-v1.6.5-RELEASE.jar 21 | 22 | # 设置上海时区 23 | RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone 24 | 25 | # 设置入口点 26 | CMD ["java", "-Xmx256m", "-Xms256m", "-jar", "ddns-v1.6.5-RELEASE.jar"] 27 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | import top.sssd.ddns.interceptor.ExcludeIndexPageInterceptor; 7 | 8 | /** 9 | * @author sssd 10 | * @careate 2023-11-17-0:07 11 | */ 12 | @Configuration 13 | public class WebConfig implements WebMvcConfigurer { 14 | @Override 15 | public void addInterceptors(InterceptorRegistry registry) { 16 | registry.addInterceptor(new ExcludeIndexPageInterceptor()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/application-mysql.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: com.mysql.cj.jdbc.Driver 4 | url: jdbc:mysql://127.0.0.1:3306/ddns4j?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai 5 | username: root 6 | password: root 7 | quartz: 8 | properties: 9 | org: 10 | quartz: 11 | scheduler: 12 | instanceName: quartzScheduler 13 | instanceId: AUTO 14 | threadPool: 15 | class: org.quartz.simpl.SimpleThreadPool 16 | threadCount: 10 17 | threadPriority: 5 18 | threadsInheritContextClassLoaderOfInitializingThread: true 19 | job-store-type: jdbc 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/application-h2.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | # h2 数据库支持 3 | datasource: 4 | driver-class-name: org.h2.Driver 5 | url: jdbc:h2:file:./ddns4j 6 | username: ddns4j 7 | password: ddns4j 8 | quartz: 9 | properties: 10 | org: 11 | quartz: 12 | scheduler: 13 | instanceName: quartzScheduler 14 | instanceId: AUTO 15 | threadPool: 16 | class: org.quartz.simpl.SimpleThreadPool 17 | threadCount: 10 18 | threadPriority: 5 19 | threadsInheritContextClassLoaderOfInitializingThread: true 20 | job-store-type: jdbc 21 | h2: 22 | console: 23 | path: /h2-console 24 | enabled: false # 线上环境务必设置为false,调试时可以开启true 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/entity/ChangedLog.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import lombok.Data; 8 | 9 | import java.io.Serializable; 10 | import java.time.LocalDateTime; 11 | 12 | /** 13 | * @author sssd 14 | * @careate 2023-10-31-16:43 15 | */ 16 | @Data 17 | @TableName("changed_log") 18 | public class ChangedLog implements Serializable { 19 | @TableId(type = IdType.AUTO) 20 | private Long id; 21 | 22 | private String content; 23 | 24 | @TableField("insert_date") 25 | private LocalDateTime insertDate; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/handler/EasySqlInjector.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.handler; 2 | 3 | import com.baomidou.mybatisplus.core.injector.AbstractMethod; 4 | import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; 5 | import com.baomidou.mybatisplus.core.metadata.TableInfo; 6 | import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 数据批量插入接口 12 | * @author sssd 13 | */ 14 | public class EasySqlInjector extends DefaultSqlInjector { 15 | @Override 16 | public List getMethodList(Class mapperClass, TableInfo tableInfo) { 17 | List methodList = super.getMethodList(mapperClass, tableInfo); 18 | // 添加InsertBatchSomeColumn方法 19 | methodList.add(new InsertBatchSomeColumn()); 20 | return methodList; 21 | } 22 | } 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/ApplicationContextProvider.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import org.springframework.context.ApplicationContext; 4 | import org.springframework.context.ApplicationContextAware; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author sssd 9 | * @created 2023-12-26-0:39 10 | */ 11 | @Component 12 | public class ApplicationContextProvider implements ApplicationContextAware { 13 | private static ApplicationContext context; 14 | 15 | @Override 16 | public void setApplicationContext(ApplicationContext applicationContext) { 17 | setContext(applicationContext); 18 | } 19 | 20 | public static synchronized ApplicationContext getContext() { 21 | return context; 22 | } 23 | 24 | public static synchronized void setContext(ApplicationContext context){ 25 | ApplicationContextProvider.context = context; 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/ResultCodeEnum.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author sssd 7 | * 统一返回结果状态信息类 8 | */ 9 | @Getter 10 | public enum ResultCodeEnum { 11 | AMIS_SUCCESS(0,"成功"), 12 | SUCCESS(200, "成功"), 13 | FAIL(201, "失败"), 14 | SERVICE_ERROR(2012, "服务异常"), 15 | DATA_ERROR(204, "数据异常"), 16 | ILLEGAL_REQUEST(205, "非法请求"), 17 | REPEAT_SUBMIT(206, "重复提交"), 18 | ARGUMENT_VALID_ERROR(210, "参数校验异常"), 19 | 20 | LOGIN_AUTH(208, "未登陆"), 21 | PERMISSION(209, "没有权限"), 22 | ACCOUNT_ERROR(214, "账号不正确"), 23 | PASSWORD_ERROR(215, "密码不正确"), 24 | LOGIN_MOBLE_ERROR(216, "账号不正确"), 25 | ACCOUNT_STOP(217, "账号已停用"), 26 | NODE_ERROR(218, "该节点下有子节点,不可以删除"); 27 | 28 | private Integer code; 29 | 30 | private String message; 31 | 32 | private ResultCodeEnum(Integer code, String message) { 33 | this.code = code; 34 | this.message = message; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.client.ClientHttpRequestFactory; 6 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | /** 10 | * @author sssd 11 | * @careate 2023-12-15-18:13 12 | */ 13 | @Configuration 14 | public class RestTemplateConfig { 15 | @Bean 16 | public RestTemplate restTemplate() { 17 | return new RestTemplate(clientHttpRequestFactory()); 18 | } 19 | 20 | @Bean 21 | public ClientHttpRequestFactory clientHttpRequestFactory() { 22 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 23 | // 连接超时时间为5秒 24 | factory.setConnectTimeout(5000); 25 | // 读取超时时间为10秒 26 | factory.setReadTimeout(10000); 27 | return factory; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/enums/ServiceProviderEnum.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.enums; 2 | 3 | /** 4 | * 服务商枚举 5 | * @author sssd 6 | * @created 2023-03-20-15:10 7 | */ 8 | public enum ServiceProviderEnum { 9 | ALI_YUN(1,"阿里云"), 10 | TENCENT(2,"腾讯云"), 11 | CLOUD_FLARE(3,"cloudflare"), 12 | HUAWEI_YUN(4,"华为云"); 13 | 14 | private Integer index; 15 | private String name; 16 | 17 | ServiceProviderEnum(){} 18 | 19 | ServiceProviderEnum(Integer index, String name) { 20 | this.index = index; 21 | this.name = name; 22 | } 23 | 24 | public Integer getIndex() { 25 | return index; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public static String getNameByIndex(Integer index) { 33 | for (ServiceProviderEnum el : ServiceProviderEnum.values()) { 34 | if (el.getIndex().equals(index)) { 35 | return el.getName(); 36 | } 37 | } 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/entity/JobTask.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import lombok.Data; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @author sssd 13 | * @created 2023-05-02-11:01 14 | */ 15 | @Data 16 | @TableName("job_task") 17 | public class JobTask implements Serializable { 18 | 19 | @TableId(type = IdType.AUTO) 20 | private Integer id; 21 | 22 | private String name; 23 | 24 | @TableField("group_name") 25 | private String groupName; 26 | 27 | @TableField("cron_expression") 28 | private String cronExpression; 29 | 30 | @TableField("class_name") 31 | private String className; 32 | 33 | private String description; 34 | 35 | private Integer status; 36 | 37 | @TableField(exist = false) 38 | private transient Object executeParams; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/enums/RecordTypeEnum.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.enums; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author sssd 8 | * @created 2023-03-20-15:27 9 | */ 10 | public enum RecordTypeEnum { 11 | IPV6(1,"AAAA"), 12 | IPV4(2,"A"); 13 | 14 | private static final Map map = new HashMap<>(); 15 | 16 | static { 17 | for (RecordTypeEnum el : RecordTypeEnum.values()) { 18 | map.put(el.getIndex(), el.getName()); 19 | } 20 | } 21 | 22 | public static String getNameByIndex(Integer index){ 23 | return map.get(index); 24 | } 25 | 26 | 27 | private Integer index; 28 | private String name; 29 | 30 | RecordTypeEnum() { 31 | } 32 | 33 | RecordTypeEnum(Integer index, String name) { 34 | this.index = index; 35 | this.name = name; 36 | } 37 | 38 | public Integer getIndex() { 39 | return index; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/IParsingRecordService.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import top.sssd.ddns.common.utils.AmisPageUtils; 5 | import top.sssd.ddns.model.entity.ParsingRecord; 6 | import top.sssd.ddns.model.response.NetWorkSelectResponse; 7 | 8 | import java.net.SocketException; 9 | import java.util.List; 10 | 11 | /** 12 | *

13 | * 解析记录表 服务类 14 | *

15 | * 16 | * @author sssd 17 | * @since 2023-03-19 18 | */ 19 | public interface IParsingRecordService extends IService { 20 | 21 | /** 22 | * 添加解析记录 23 | * @param parsingRecord 24 | */ 25 | void add(ParsingRecord parsingRecord) throws Exception; 26 | 27 | void modify(ParsingRecord parsingRecord) throws Exception; 28 | 29 | void delete(Long id) throws Exception; 30 | 31 | AmisPageUtils queryPage(ParsingRecord parsingRecord); 32 | 33 | String getIp(ParsingRecord parsingRecord); 34 | 35 | 36 | List getModeIpValue(Integer getIpMode,Integer recordType) throws SocketException; 37 | 38 | void copy(ParsingRecord parsingRecord) throws Exception; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/handler/MyMetaObjectHandler.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.handler; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.ibatis.reflection.MetaObject; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | import static top.sssd.ddns.common.constant.DDNSConstant.*; 11 | 12 | /** 13 | * @author sssd 14 | */ 15 | @Component 16 | @Slf4j 17 | public class MyMetaObjectHandler implements MetaObjectHandler { 18 | 19 | /** 20 | * @author sssd 21 | */ 22 | 23 | 24 | @Override 25 | public void insertFill(MetaObject metaObject) { 26 | this.strictInsertFill(metaObject, CREATE_DATE, LocalDateTime.class, LocalDateTime.now()); 27 | this.strictInsertFill(metaObject, UPDATE_DATE, LocalDateTime.class, LocalDateTime.now()); 28 | 29 | this.strictInsertFill(metaObject, CREATOR, Long.class, 0L); 30 | this.strictInsertFill(metaObject, UPDATER, Long.class, 0L); 31 | 32 | } 33 | 34 | @Override 35 | public void updateFill(MetaObject metaObject) { 36 | this.strictUpdateFill(metaObject,UPDATE_DATE,LocalDateTime.class, LocalDateTime.now()); 37 | this.strictUpdateFill(metaObject,UPDATER,Long.class, 0L); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.workflow/pr-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: pr-pipeline 3 | displayName: PRPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@maven 10 | name: build_maven 11 | displayName: Maven 构建 12 | # 支持6、7、8、9、10、11六个版本 13 | jdkVersion: 8 14 | # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 15 | mavenVersion: 3.3.9 16 | # 构建命令 17 | commands: 18 | - mvn -B clean package -Dmaven.test.skip=true 19 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 20 | artifacts: 21 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 22 | - name: BUILD_ARTIFACT 23 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 24 | path: 25 | - ./target 26 | - step: publish@general_artifacts 27 | name: publish_general_artifacts 28 | displayName: 上传制品 29 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 30 | dependArtifact: BUILD_ARTIFACT 31 | # 构建产物制品库,默认default,系统默认创建 32 | artifactRepository: default 33 | # 上传到制品库时的制品命名,默认output 34 | artifactName: output 35 | dependsOn: build_maven 36 | triggers: 37 | pr: 38 | branches: 39 | include: 40 | - master 41 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/DynamicDnsServiceFactory.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | import top.sssd.ddns.config.DnsServiceTypeConfig; 8 | 9 | import javax.annotation.Resource; 10 | import java.util.Map; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | /** 14 | * @author sssd 15 | * @created 2023-05-06-17:07 16 | */ 17 | @Component 18 | public class DynamicDnsServiceFactory implements ApplicationContextAware { 19 | 20 | private static Map dnsStrategyMap = new ConcurrentHashMap<>(); 21 | 22 | @Resource 23 | private DnsServiceTypeConfig dnsServiceTypeConfig; 24 | 25 | private DynamicDnsServiceFactory(){} 26 | 27 | public DynamicDnsStrategy getServiceInstance(Integer serviceProvider) { 28 | return dnsStrategyMap.get(serviceProvider); 29 | } 30 | 31 | @Override 32 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 33 | dnsServiceTypeConfig.getServiceTypes().forEach((k,v)->dnsStrategyMap.put(k,(DynamicDnsStrategy)applicationContext.getBean(v))); 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/IJobTaskService.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import top.sssd.ddns.model.entity.JobTask; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author sssd 10 | * @created 2023-05-02-11:05 11 | */ 12 | public interface IJobTaskService extends IService { 13 | /** 14 | * 添加任务 15 | * @param jobTask 任务实体 16 | * @return boolean 17 | */ 18 | boolean addJobTask(JobTask jobTask); 19 | 20 | /** 21 | * 更新任务 22 | * @param jobTask 任务实体 23 | * @return boolean 24 | */ 25 | boolean updateJobTask(JobTask jobTask); 26 | 27 | /** 28 | * 删除任务 29 | * @param id 任务ID 30 | * @return boolean 31 | */ 32 | boolean deleteJobTask(Integer id); 33 | 34 | /** 35 | * 获取指定任务 36 | * @param id 任务ID 37 | * @return JobTask 38 | */ 39 | JobTask getJobTaskById(Integer id); 40 | 41 | /** 42 | * 获取所有任务列表 43 | * @return List 44 | */ 45 | List listAllJobTasks(); 46 | 47 | /** 48 | * 启动任务 49 | * @param id 任务ID 50 | * @return boolean 51 | */ 52 | boolean startJobTask(Integer id); 53 | 54 | /** 55 | * 停止任务 56 | * @param id 任务ID 57 | * @return boolean 58 | */ 59 | boolean stopJobTask(Integer id); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/utils/AmisPageUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.utils; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * @author xuyang13 10 | */ 11 | public class AmisPageUtils implements Serializable { 12 | private static final long serialVersionUID = 1L; 13 | 14 | /** 15 | * 总条数 16 | */ 17 | private int total; 18 | 19 | /** 20 | * 当前页码 21 | */ 22 | private int page; 23 | 24 | /** 25 | * 返回的数据实体 26 | */ 27 | private List items; 28 | 29 | /** 30 | * 分页 31 | */ 32 | public AmisPageUtils(IPage page) { 33 | this.items = page.getRecords(); 34 | this.total = (int)page.getTotal(); 35 | this.page = (int)page.getCurrent(); 36 | } 37 | 38 | public AmisPageUtils(){} 39 | 40 | public int getTotal() { 41 | return total; 42 | } 43 | 44 | public void setTotal(int total) { 45 | this.total = total; 46 | } 47 | 48 | public int getPage() { 49 | return page; 50 | } 51 | 52 | public void setPage(int page) { 53 | this.page = page; 54 | } 55 | 56 | public List getItems() { 57 | return items; 58 | } 59 | 60 | public void setItems(List items) { 61 | this.items = items; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/controller/PublicAccessController.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.controller; 2 | 3 | import lombok.Data; 4 | import org.springframework.web.bind.annotation.*; 5 | import top.sssd.ddns.common.AmisResult; 6 | 7 | import java.util.HashMap; 8 | 9 | import static top.sssd.ddns.common.constant.DDNSConstant.PUBLIC_ACCESS_DISABLED_KEY; 10 | import static top.sssd.ddns.common.constant.DDNSConstant.publicAccessDisabledMap; 11 | 12 | /** 13 | * @author sssd 14 | * @careate 2024-01-01-18:08 15 | */ 16 | @RestController 17 | @RequestMapping("publicAccess") 18 | public class PublicAccessController { 19 | 20 | // 禁止公网访问开关相关接口-start 21 | @Data 22 | static class PublicAccessDisabled{ 23 | private Boolean value; 24 | } 25 | 26 | @PostMapping("publicAccessDisabled") 27 | public AmisResult UpdatePublicAccessDisabled(@RequestBody PublicAccessDisabled publicAccessDisabled){ 28 | publicAccessDisabledMap.put(PUBLIC_ACCESS_DISABLED_KEY,publicAccessDisabled.getValue()); 29 | return AmisResult.ok(); 30 | } 31 | 32 | 33 | @GetMapping("publicAccessDisabled") 34 | public AmisResult> publicAccessDisabled(){ 35 | Boolean publicAccessDisabled = publicAccessDisabledMap.get(PUBLIC_ACCESS_DISABLED_KEY); 36 | HashMap resultMap = new HashMap<>(); 37 | resultMap.put("publicAccessDisabled",publicAccessDisabled); 38 | return AmisResult.ok(resultMap); 39 | } 40 | // 禁止公网访问开关相关接口-end 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/task/ClearLogJob.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.task; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.quartz.Job; 5 | import org.quartz.JobExecutionContext; 6 | import org.quartz.JobExecutionException; 7 | import org.springframework.util.CollectionUtils; 8 | import top.sssd.ddns.model.entity.ChangedLog; 9 | import top.sssd.ddns.service.ChangedLogService; 10 | 11 | import javax.annotation.Resource; 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * @author sssd 18 | * @careate 2023-11-13-23:47 19 | */ 20 | @Slf4j 21 | public class ClearLogJob implements Job { 22 | 23 | @Resource 24 | private ChangedLogService changedLogService; 25 | 26 | @Override 27 | public void execute(JobExecutionContext context) throws JobExecutionException { 28 | log.info("每小时清除日志开始..."); 29 | LocalDateTime now = LocalDateTime.now(); 30 | LocalDateTime dayAog = now.minusDays(3); 31 | List list = changedLogService.lambdaQuery() 32 | .le(ChangedLog::getInsertDate, dayAog) 33 | .orderByDesc(ChangedLog::getInsertDate) 34 | .list(); 35 | if (CollectionUtils.isEmpty(list)){ 36 | log.info("每小时清除日志结束..."); 37 | return; 38 | } 39 | List ids = list.stream().map(ChangedLog::getId).collect(Collectors.toList()); 40 | changedLogService.removeBatchByIds(ids); 41 | log.info("每小时清除日志结束..."); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/StartupRunner.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import org.springframework.boot.CommandLineRunner; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.util.CollectionUtils; 6 | import top.sssd.ddns.model.entity.JobTask; 7 | import top.sssd.ddns.service.IJobTaskService; 8 | import top.sssd.ddns.task.ClearLogJob; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.List; 12 | 13 | /** 14 | * @author sssd 15 | * @careate 2023-11-14-0:17 16 | */ 17 | @Component 18 | public class StartupRunner implements CommandLineRunner { 19 | 20 | @Resource 21 | private IJobTaskService jobTaskService; 22 | 23 | @Override 24 | public void run(String... args) throws Exception { 25 | List list = jobTaskService.lambdaQuery() 26 | .eq(JobTask::getClassName, ClearLogJob.class.getName()) 27 | .eq(JobTask::getStatus, 1).list(); 28 | 29 | if (CollectionUtils.isEmpty(list)) { 30 | addWithStartTask(); 31 | return; 32 | } 33 | jobTaskService.startJobTask(list.get(0).getId()); 34 | } 35 | 36 | 37 | public void addWithStartTask() { 38 | JobTask jobTask = new JobTask(); 39 | jobTask.setName("clearLogJob"); 40 | jobTask.setStatus(1); 41 | jobTask.setClassName(ClearLogJob.class.getName()); 42 | //每小时执行一次 43 | jobTask.setCronExpression("0 0 0/1 * * ? "); 44 | jobTask.setExecuteParams(null); 45 | jobTaskService.addJobTask(jobTask); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/constant/DDNSConstant.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.constant; 2 | 3 | import java.util.HashMap; 4 | 5 | /** 6 | * DDNS常量类 7 | * 8 | * @author sssd 9 | */ 10 | public class DDNSConstant { 11 | private DDNSConstant() { 12 | } 13 | 14 | public static final String CREATE_DATE = "createDate"; 15 | public static final String UPDATE_DATE = "updateDate"; 16 | public static final String CREATOR = "creator"; 17 | public static final String UPDATER = "updater"; 18 | public static final String[] IPV4_INTERFACE_VALUES = {"https://ip.3322.net", "https://4.ipw.cn"}; 19 | public static final String[] IPV6_INTERFACE_VALUES = 20 | {"https://v6.ip.zxinc.org/getip", 21 | "https://api6.ipify.org", 22 | // "https://api.ip.sb/ip", 23 | // "https://api.myip.la", 24 | "https://speed.neu6.edu.cn/getIP.php", 25 | "https://v6.ident.me", 26 | "https://6.ipw.cn"}; 27 | public static final Integer RECORD_TYPE_AAAA = 1; 28 | public static final Integer RECORD_TYPE_A = 2; 29 | 30 | public static final Integer IP_MODE_INTERFACE = 1; 31 | public static final Integer IP_MODE_NETWORK = 2; 32 | 33 | public static final String PUBLIC_ACCESS_DISABLED_KEY = "ddns4j.publicAccessDisabled"; 34 | 35 | public static HashMap publicAccessDisabledMap = new HashMap(); 36 | 37 | static { 38 | //默认可以公网访问 39 | publicAccessDisabledMap.put(PUBLIC_ACCESS_DISABLED_KEY,false); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.workflow/master-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: master-pipeline 3 | displayName: MasterPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@maven 10 | name: build_maven 11 | displayName: Maven 构建 12 | # 支持6、7、8、9、10、11六个版本 13 | jdkVersion: 8 14 | # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 15 | mavenVersion: 3.3.9 16 | # 构建命令 17 | commands: 18 | - mvn -B clean package -Dmaven.test.skip=true 19 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 20 | artifacts: 21 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 22 | - name: BUILD_ARTIFACT 23 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 24 | path: 25 | - ./target 26 | - step: publish@general_artifacts 27 | name: publish_general_artifacts 28 | displayName: 上传制品 29 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 30 | dependArtifact: BUILD_ARTIFACT 31 | # 上传到制品库时的制品命名,默认output 32 | artifactName: output 33 | dependsOn: build_maven 34 | - stage: 35 | name: release 36 | displayName: 发布 37 | steps: 38 | - step: publish@release_artifacts 39 | name: publish_release_artifacts 40 | displayName: '发布' 41 | # 上游上传制品任务的产出 42 | dependArtifact: output 43 | # 发布制品版本号 44 | version: '1.0.0.0' 45 | # 是否开启版本号自增,默认开启 46 | autoIncrement: true 47 | triggers: 48 | push: 49 | branches: 50 | include: 51 | - master 52 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/utils/DoMainUtil.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.utils; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * 域名工具类 7 | * @author sssd 8 | * @created 2023-03-20-14:58 9 | */ 10 | public class DoMainUtil { 11 | private DoMainUtil() { 12 | } 13 | 14 | public static boolean firstLevel(String domain) { 15 | if (Objects.isNull(domain) || domain.isEmpty()) { 16 | return false; 17 | } 18 | int count = 0; 19 | for (char aChar : domain.toCharArray()) { 20 | if (aChar == '.') { 21 | count++; 22 | } 23 | } 24 | return count == 1; 25 | } 26 | 27 | public static String[] spiltDomain(String domain){ 28 | String resultDomain = ""; 29 | String subDoMain = ""; 30 | if (DoMainUtil.firstLevel(domain)) { 31 | subDoMain = "@"; 32 | } else { 33 | resultDomain = domain.substring(domain.indexOf('.') + 1); 34 | subDoMain = domain.substring(0, domain.indexOf('.')); 35 | } 36 | return new String[]{resultDomain,subDoMain}; 37 | } 38 | 39 | public static int findNthOccurrence(String str, String subStr, int n) { 40 | // 记录出现次数 41 | int count = 0; 42 | // 从后往前查找最后一次出现的位置 43 | int index = str.lastIndexOf(subStr); 44 | // 如果找到了并且出现次数小于n 45 | while (index != -1 && count < n) { 46 | // 继续往前查找下一次出现的位置 47 | index = str.lastIndexOf(subStr, index - 1); 48 | // 更新出现次数 49 | count++; 50 | } 51 | // 返回最后一次出现的位置的索引 52 | return index; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /.workflow/branch-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: branch-pipeline 3 | displayName: BranchPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@maven 10 | name: build_maven 11 | displayName: Maven 构建 12 | # 支持6、7、8、9、10、11六个版本 13 | jdkVersion: 8 14 | # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 15 | mavenVersion: 3.3.9 16 | # 构建命令 17 | commands: 18 | - mvn -B clean package -Dmaven.test.skip=true 19 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 20 | artifacts: 21 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 22 | - name: BUILD_ARTIFACT 23 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 24 | path: 25 | - ./target 26 | - step: publish@general_artifacts 27 | name: publish_general_artifacts 28 | displayName: 上传制品 29 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 30 | dependArtifact: BUILD_ARTIFACT 31 | # 上传到制品库时的制品命名,默认output 32 | artifactName: output 33 | dependsOn: build_maven 34 | - stage: 35 | name: release 36 | displayName: 发布 37 | steps: 38 | - step: publish@release_artifacts 39 | name: publish_release_artifacts 40 | displayName: '发布' 41 | # 上游上传制品任务的产出 42 | dependArtifact: output 43 | # 发布制品版本号 44 | version: '1.0.0.0' 45 | # 是否开启版本号自增,默认开启 46 | autoIncrement: true 47 | triggers: 48 | push: 49 | branches: 50 | exclude: 51 | - master 52 | include: 53 | - .* 54 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/MybatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import top.sssd.ddns.handler.EasySqlInjector; 10 | 11 | /** 12 | * @author sssd 13 | */ 14 | @Configuration 15 | public class MybatisPlusConfig { 16 | 17 | /** 18 | * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除) 19 | */ 20 | @Bean 21 | @ConditionalOnProperty(name = "spring.datasource.driver-class-name", havingValue = "com.mysql.cj.jdbc.Driver") 22 | public MybatisPlusInterceptor mybatisPlusInterceptorForMysql() { 23 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 24 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); 25 | return interceptor; 26 | } 27 | 28 | @Bean 29 | @ConditionalOnProperty(name = "spring.datasource.driver-class-name", havingValue = "org.h2.Driver") 30 | public MybatisPlusInterceptor mybatisPlusInterceptorForH2() { 31 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 32 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); 33 | return interceptor; 34 | } 35 | 36 | @Bean 37 | public EasySqlInjector easySqlInjector() { 38 | return new EasySqlInjector(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/enums/UpdateFrequencyEnum.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author sssd 8 | * @created 2023-05-02-11:37 9 | */ 10 | @AllArgsConstructor 11 | @Getter 12 | public enum UpdateFrequencyEnum { 13 | 14 | ONE_MINUTE(1, "1分钟", "0 */1 * * * ?"), 15 | TWO_MINUTES(2, "2分钟", "0 */2 * * * ?"), 16 | FIVE_MINUTES(5, "5分钟", "0 */5 * * * ?"), 17 | TEN_MINUTES(10, "10分钟", "0 */10 * * * ?"); 18 | 19 | private final Integer code; 20 | private final String desc; 21 | private final String cronExpression; 22 | 23 | /** 24 | * 通过 code 获取 desc 25 | * @param code 26 | * @return 27 | */ 28 | public static String getDescByCode(Integer code) { 29 | for (UpdateFrequencyEnum e : UpdateFrequencyEnum.values()) { 30 | if (e.code.equals(code)) { 31 | return e.desc; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | /** 38 | * 通过 desc 获取 code 39 | * @param desc 40 | * @return 41 | */ 42 | public static Integer getCodeByDesc(String desc) { 43 | for (UpdateFrequencyEnum e : UpdateFrequencyEnum.values()) { 44 | if (e.desc.equals(desc)) { 45 | return e.code; 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | /** 52 | * 通过 code 获取 cron 表达式 53 | * @param code 54 | * @return 55 | */ 56 | public static String getCronExpressionByCode(Integer code) { 57 | for (UpdateFrequencyEnum e : UpdateFrequencyEnum.values()) { 58 | if (e.code.equals(code)) { 59 | return e.cronExpression; 60 | } 61 | } 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/controller/ChangedLogController.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.controller; 2 | 3 | import org.springframework.util.CollectionUtils; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | import top.sssd.ddns.model.entity.ChangedLog; 8 | import top.sssd.ddns.service.ChangedLogService; 9 | 10 | import javax.annotation.Resource; 11 | import java.time.LocalDateTime; 12 | import java.time.format.DateTimeFormatter; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * @author sssd 18 | * @careate 2024-01-01-18:10 19 | */ 20 | @RestController 21 | @RequestMapping("changedLog") 22 | public class ChangedLogController { 23 | 24 | @Resource 25 | private ChangedLogService changedLogService; 26 | @PostMapping("logs") 27 | public String logs() throws Exception { 28 | LocalDateTime now = LocalDateTime.now(); 29 | LocalDateTime dayAog = now.minusDays(1); 30 | List list = changedLogService.lambdaQuery() 31 | .le(ChangedLog::getInsertDate, now) 32 | .ge(ChangedLog::getInsertDate, dayAog) 33 | .orderByDesc(ChangedLog::getInsertDate) 34 | .list(); 35 | if(CollectionUtils.isEmpty(list)){ 36 | return null; 37 | } 38 | List result = list.stream().map(item -> { 39 | StringBuilder builder = new StringBuilder(); 40 | builder.append(item.getInsertDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); 41 | builder.append(":"); 42 | builder.append(item.getContent()); 43 | return builder.toString(); 44 | }).collect(Collectors.toList()); 45 | 46 | return String.join("\r\n",result); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/Result.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Objects; 8 | /** 9 | * @author sssd 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class Result{ 15 | /** 16 | * 返回码 17 | */ 18 | private Integer code; 19 | 20 | /** 21 | * 返回消息 22 | */ 23 | private String message; 24 | 25 | /** 26 | * 返回数据 27 | */ 28 | private T data; 29 | 30 | protected static Result build(T data) { 31 | Result result = new Result<>(); 32 | if (Objects.nonNull(data)) { 33 | result.setData(data); 34 | } 35 | return result; 36 | } 37 | 38 | public static Result build(T body, Integer code, String message) { 39 | Result result = build(body); 40 | result.setCode(code); 41 | result.setMessage(message); 42 | return result; 43 | } 44 | 45 | public static Result build(T body, ResultCodeEnum resultCodeEnum) { 46 | Result result = build(body); 47 | result.setCode(resultCodeEnum.getCode()); 48 | result.setMessage(resultCodeEnum.getMessage()); 49 | return result; 50 | } 51 | 52 | public static Result ok(T data){ 53 | return build(data, ResultCodeEnum.SUCCESS); 54 | } 55 | 56 | public static Result ok(){ 57 | return Result.ok(null); 58 | } 59 | 60 | public static Result fail(T data){ 61 | return build(data, ResultCodeEnum.FAIL); 62 | } 63 | 64 | public static Result fail(){ 65 | return Result.fail(null); 66 | } 67 | 68 | public Result message(String msg){ 69 | this.setMessage(msg); 70 | return this; 71 | } 72 | 73 | public Result code(Integer code){ 74 | this.setCode(code); 75 | return this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/DynamicDnsStrategy.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | import top.sssd.ddns.model.entity.ParsingRecord; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.NotNull; 7 | 8 | /** 9 | * @author sssd 10 | * @created 2023-03-19-21:43 11 | */ 12 | public interface DynamicDnsStrategy { 13 | 14 | /** 15 | * 通过域名及密钥判断是否存在 16 | * @param serviceProviderId 17 | * @param serviceProviderSecret 18 | * @param subDomain 19 | * @param recordType 20 | * @return 21 | */ 22 | boolean exist(@NotBlank(message = "传入的serviceProviderId不能为空") String serviceProviderId, 23 | String serviceProviderSecret,@NotBlank(message = "传入的子域名不能为空") String subDomain, 24 | @NotBlank(message = "传入的解析类型不能为空") String recordType) throws Exception; 25 | 26 | /** 27 | * 新增解析记录 28 | * 29 | * @param parsingRecord 解析对象 30 | */ 31 | void add(@NotNull(message = "传入的解析对象不能为空") ParsingRecord parsingRecord,String ip) throws Exception; 32 | 33 | /** 34 | * 更新解析记录 35 | * 36 | * @param parsingRecord 解析对象 37 | */ 38 | void update(@NotNull(message = "传入的解析对象不能为空") ParsingRecord parsingRecord,String ip,String recordId) throws Exception; 39 | 40 | 41 | /** 42 | * 获取服务上解析记录ID 43 | * @param parsingRecord 44 | * @param ip 45 | * @return 46 | */ 47 | String getRecordId(@NotNull(message = "传入的解析对象不能为空") ParsingRecord parsingRecord,String ip) throws Exception; 48 | 49 | /** 50 | * 根据解析记录Id删除记录 51 | * @param parsingRecord 52 | * @param ip 53 | */ 54 | void remove(@NotNull(message = "传入的解析对象不能为空") ParsingRecord parsingRecord, String ip) throws Exception; 55 | 56 | 57 | /** 58 | * 根据解析记录获取服务商中的ip 59 | * @param parsingRecord 60 | * @return 61 | */ 62 | String getIpBySubDomainWithType(@NotNull(message = "传入的解析对象不能为空") ParsingRecord parsingRecord) throws Exception; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/H2Initializer.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.jdbc.datasource.init.ScriptUtils; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.annotation.Resource; 12 | import java.sql.SQLException; 13 | 14 | /** 15 | * @author sssd 16 | * @careate 2023-09-26-15:40 17 | */ 18 | @Slf4j 19 | @Component 20 | @ConditionalOnProperty(name = "spring.datasource.driver-class-name", havingValue = "org.h2.Driver") 21 | public class H2Initializer { 22 | 23 | private static final String TABLE_EXIST_SQL = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?"; 24 | 25 | private static final String TABLE_JOB_TASK = "job_task"; 26 | 27 | private static final String TABLE_PARSING_RECORD = "parsing_record"; 28 | 29 | private static final String MYSQL_SCRIPT_PATH = "sql/ddns4j_h2.sql"; 30 | 31 | @Resource 32 | private JdbcTemplate jdbcTemplate; 33 | 34 | 35 | @PostConstruct 36 | public void initSQL() throws SQLException { 37 | log.info("开始检查项目是否是第一次启动"); 38 | if (!isTableExists(TABLE_JOB_TASK) && !isTableExists(TABLE_PARSING_RECORD)) { 39 | log.info("初始化h2数据库脚本开始..."); 40 | executeScript(MYSQL_SCRIPT_PATH); 41 | log.info("初始化h2数据库脚本结束..."); 42 | } 43 | } 44 | 45 | private boolean isTableExists(String tableName) { 46 | Integer count = jdbcTemplate.queryForObject(TABLE_EXIST_SQL, Integer.class, tableName); 47 | return count != null && count > 0; 48 | } 49 | 50 | private void executeScript(String scriptPath) throws SQLException { 51 | ClassPathResource resource = new ClassPathResource(scriptPath); 52 | ScriptUtils.executeSqlScript(jdbcTemplate.getDataSource().getConnection(), resource); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/config/MySQLInitializer.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.jdbc.datasource.init.ScriptUtils; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.annotation.Resource; 12 | import java.sql.SQLException; 13 | 14 | /** 15 | * @author sssd 16 | * @careate 2023-09-26-15:40 17 | */ 18 | @Slf4j 19 | @Component 20 | @ConditionalOnProperty(name = "spring.datasource.driver-class-name", havingValue = "com.mysql.cj.jdbc.Driver") 21 | public class MySQLInitializer { 22 | 23 | private static final String TABLE_EXIST_SQL = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?"; 24 | 25 | private static final String TABLE_JOB_TASK = "job_task"; 26 | 27 | private static final String TABLE_PARSING_RECORD = "parsing_record"; 28 | 29 | private static final String MYSQL_SCRIPT_PATH = "sql/ddns4j_mysql.sql"; 30 | 31 | @Resource 32 | private JdbcTemplate jdbcTemplate; 33 | 34 | 35 | @PostConstruct 36 | public void initSQL() throws SQLException { 37 | log.info("开始检查项目是否是第一次启动"); 38 | if (!isTableExists(TABLE_JOB_TASK) && !isTableExists(TABLE_PARSING_RECORD)) { 39 | log.info("初始化mysql数据库脚本开始..."); 40 | executeScript(MYSQL_SCRIPT_PATH); 41 | log.info("初始化mysql数据库脚本结束..."); 42 | } 43 | } 44 | 45 | private boolean isTableExists(String tableName) { 46 | Integer count = jdbcTemplate.queryForObject(TABLE_EXIST_SQL, Integer.class, tableName); 47 | return count != null && count > 0; 48 | } 49 | 50 | private void executeScript(String scriptPath) throws SQLException { 51 | ClassPathResource resource = new ClassPathResource(scriptPath); 52 | ScriptUtils.executeSqlScript(jdbcTemplate.getDataSource().getConnection(), resource); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/AmisResult.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * @author sssd 11 | * @careate 2023-10-25-15:04 12 | */ 13 | @Data 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class AmisResult { 17 | 18 | private Integer status; 19 | 20 | private String msg; 21 | 22 | private T data; 23 | 24 | protected static AmisResult build(T data) { 25 | AmisResult result = new AmisResult<>(); 26 | if (Objects.nonNull(data)) { 27 | result.setData(data); 28 | } 29 | return result; 30 | } 31 | 32 | public static AmisResult build(T body, Integer code, String message) { 33 | AmisResult result = build(body); 34 | result.setStatus(code); 35 | result.setMsg(message); 36 | return result; 37 | } 38 | 39 | public static AmisResult build(T body, ResultCodeEnum resultCodeEnum) { 40 | AmisResult result = build(body); 41 | result.setStatus(resultCodeEnum.getCode()); 42 | result.setMsg(resultCodeEnum.getMessage()); 43 | return result; 44 | } 45 | 46 | public static AmisResult build(ResultCodeEnum resultCodeEnum,String message) { 47 | AmisResult result = new AmisResult<>(); 48 | result.setStatus(resultCodeEnum.getCode()); 49 | result.setMsg(message); 50 | return result; 51 | } 52 | 53 | public static AmisResult ok(T data){ 54 | return build(data, ResultCodeEnum.AMIS_SUCCESS); 55 | } 56 | 57 | public static AmisResult ok(){ 58 | return AmisResult.ok(null); 59 | } 60 | 61 | 62 | public static AmisResult fail(String message){ 63 | return build(ResultCodeEnum.FAIL, message); 64 | } 65 | 66 | public static AmisResult fail(){ 67 | return AmisResult.fail(null); 68 | } 69 | 70 | public AmisResult message(String msg){ 71 | this.setMsg(msg); 72 | return this; 73 | } 74 | 75 | public AmisResult code(Integer code){ 76 | this.setStatus(code); 77 | return this; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/interceptor/ExcludeIndexPageInterceptor.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.interceptor; 2 | 3 | import org.springframework.web.servlet.HandlerInterceptor; 4 | import sun.net.util.IPAddressUtil; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.net.InetAddress; 9 | import java.net.UnknownHostException; 10 | 11 | import static top.sssd.ddns.common.constant.DDNSConstant.*; 12 | 13 | /** 14 | * @author sssd 15 | * @careate 2023-11-17-0:03 16 | */ 17 | public class ExcludeIndexPageInterceptor implements HandlerInterceptor{ 18 | 19 | @Override 20 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 21 | Boolean publicAccessDisabled = publicAccessDisabledMap.get(PUBLIC_ACCESS_DISABLED_KEY); 22 | if (publicAccessDisabled) { 23 | String remoteAddr = request.getRemoteAddr(); 24 | String requestURI = request.getRequestURI(); 25 | 26 | if (IPAddressUtil.isIPv6LiteralAddress(remoteAddr)) { 27 | if(!isIPv6Private(remoteAddr) && requestURI.equals("/index.html")){ 28 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 29 | return false; 30 | } 31 | }else{ 32 | boolean innerIP = isInternalIP(remoteAddr); 33 | if (!innerIP && requestURI.equals("/index.html")) { 34 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 35 | return false; 36 | } 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | public boolean isIPv6Private(String ipAddress) { 43 | try { 44 | InetAddress inetAddress = InetAddress.getByName(ipAddress); 45 | return inetAddress.isSiteLocalAddress() || inetAddress.isLinkLocalAddress() || inetAddress.isLoopbackAddress(); 46 | } catch (UnknownHostException e) { 47 | e.printStackTrace(); 48 | return false; 49 | } 50 | } 51 | 52 | public boolean isInternalIP(String ipAddress) { 53 | try { 54 | InetAddress inetAddress = InetAddress.getByName(ipAddress); 55 | return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress() || inetAddress.isSiteLocalAddress(); 56 | } catch (UnknownHostException e) { 57 | System.out.println("无法解析 IP 地址: " + ipAddress); 58 | return false; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/impl/NetWorkServiceImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service.impl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.stereotype.Service; 5 | import top.sssd.ddns.model.response.NetWorkSelectResponse; 6 | import top.sssd.ddns.service.NetWorkService; 7 | 8 | import java.net.*; 9 | import java.util.Enumeration; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | import static top.sssd.ddns.common.constant.DDNSConstant.RECORD_TYPE_A; 14 | import static top.sssd.ddns.common.constant.DDNSConstant.RECORD_TYPE_AAAA; 15 | 16 | /** 17 | * @author sssd 18 | * @created 2023-09-22-13:59 19 | */ 20 | @Service 21 | @Slf4j 22 | public class NetWorkServiceImpl implements NetWorkService { 23 | 24 | @Override 25 | public List networks(Integer recordType) throws SocketException { 26 | LinkedList networkList = new LinkedList<>(); 27 | Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); 28 | while (interfaces.hasMoreElements()) { 29 | NetworkInterface iface = interfaces.nextElement(); 30 | Enumeration addresses = iface.getInetAddresses(); 31 | while (addresses.hasMoreElements()) { 32 | InetAddress addr = addresses.nextElement(); 33 | if (recordType.equals(RECORD_TYPE_A) && addr instanceof Inet4Address) { 34 | NetWorkSelectResponse netWorkSelectResponse = new NetWorkSelectResponse(); 35 | netWorkSelectResponse.setLabel(iface.getName()+"-"+addr.getHostAddress()); 36 | netWorkSelectResponse.setValue(addr.getHostAddress()); 37 | networkList.add(netWorkSelectResponse); 38 | log.info("IPv4:{}",addr.getHostAddress()); 39 | } else if (recordType.equals(RECORD_TYPE_AAAA) && addr instanceof Inet6Address) { 40 | String hostAddress = addr.getHostAddress(); 41 | if (hostAddress.contains("%")) { 42 | continue; 43 | } 44 | NetWorkSelectResponse netWorkSelectResponse = new NetWorkSelectResponse(); 45 | netWorkSelectResponse.setLabel(iface.getName()+"-"+addr.getHostAddress()); 46 | netWorkSelectResponse.setValue(hostAddress); 47 | networkList.add(netWorkSelectResponse); 48 | log.info("IPv6:{}",addr.getHostAddress()); 49 | } 50 | } 51 | } 52 | return networkList; 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/task/DynamicDnsJob.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.task; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.quartz.Job; 5 | import org.quartz.JobExecutionContext; 6 | import top.sssd.ddns.strategy.DynamicDnsServiceFactory; 7 | import top.sssd.ddns.model.entity.ChangedLog; 8 | import top.sssd.ddns.model.entity.ParsingRecord; 9 | import top.sssd.ddns.service.ChangedLogService; 10 | import top.sssd.ddns.strategy.DynamicDnsStrategy; 11 | import top.sssd.ddns.service.IParsingRecordService; 12 | 13 | import javax.annotation.Resource; 14 | import java.time.LocalDateTime; 15 | 16 | /** 17 | * @author sssd 18 | * @created 2023-05-02-11:51 19 | */ 20 | @Slf4j 21 | public class DynamicDnsJob implements Job { 22 | 23 | 24 | @Resource 25 | private IParsingRecordService parsingRecordService; 26 | 27 | @Resource 28 | private ChangedLogService changedLogService; 29 | 30 | @Resource 31 | private DynamicDnsServiceFactory dnsServiceFactory; 32 | 33 | 34 | @Override 35 | public void execute(JobExecutionContext context) { 36 | Object executeParams = context.getJobDetail().getJobDataMap().get("executeParams"); 37 | ParsingRecord parsingRecord = (ParsingRecord) executeParams; 38 | 39 | DynamicDnsStrategy dynamicDnsService = dnsServiceFactory.getServiceInstance(parsingRecord.getServiceProvider()); 40 | String dnsIp = null; 41 | try { 42 | dnsIp = dynamicDnsService.getIpBySubDomainWithType(parsingRecord); 43 | } catch (Exception e) { 44 | e.printStackTrace(); 45 | } 46 | 47 | String nowIp = parsingRecordService.getIp(parsingRecord); 48 | if(nowIp.equals(dnsIp)){ 49 | log.info("域名为:{}的记录,域名服务商中的ip:{}与现在的ip:{},未发生改变",parsingRecord.getDomain(),dnsIp,nowIp); 50 | String content = String.format("域名为:%s的记录,未发生改变", parsingRecord.getDomain()); 51 | insertLog(content); 52 | return; 53 | } 54 | try { 55 | parsingRecordService.modify(parsingRecord); 56 | } catch (Exception e) { 57 | e.printStackTrace(); 58 | } 59 | log.info("域名为:{}的记录,已将域名服务商中的ip:{},修改为现在的ip:{},更新成功",parsingRecord.getDomain(),dnsIp,nowIp); 60 | String content = String.format("域名为:%s的记录,更新成功",parsingRecord.getDomain()); 61 | insertLog(content); 62 | } 63 | 64 | 65 | public void insertLog(String content){ 66 | ChangedLog changedLog = new ChangedLog(); 67 | changedLog.setContent(content); 68 | changedLog.setInsertDate(LocalDateTime.now()); 69 | changedLogService.save(changedLog); 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/controller/ParsingRecordController.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.controller; 2 | 3 | import org.springframework.validation.annotation.Validated; 4 | import org.springframework.web.bind.annotation.*; 5 | import top.sssd.ddns.common.AmisResult; 6 | import top.sssd.ddns.common.utils.AmisPageUtils; 7 | import top.sssd.ddns.common.valid.ValidGroup; 8 | import top.sssd.ddns.model.entity.ParsingRecord; 9 | import top.sssd.ddns.model.response.NetWorkSelectResponse; 10 | import top.sssd.ddns.service.IParsingRecordService; 11 | 12 | import javax.annotation.Resource; 13 | import java.util.List; 14 | 15 | /** 16 | * @author sssd 17 | * @careate 2023-10-25-15:01 18 | */ 19 | @RestController 20 | @RequestMapping("parsingRecord") 21 | public class ParsingRecordController { 22 | 23 | @Resource 24 | private IParsingRecordService parsingRecordService; 25 | 26 | @GetMapping("page") 27 | public AmisResult> queryPage(ParsingRecord parsingRecord){ 28 | AmisPageUtils pageResult = parsingRecordService.queryPage(parsingRecord); 29 | return AmisResult.ok(pageResult); 30 | } 31 | 32 | @PostMapping("add") 33 | public AmisResult add(@RequestBody 34 | @Validated(ValidGroup.SaveGroup.class) ParsingRecord parsingRecord) throws Exception { 35 | parsingRecordService.add(parsingRecord); 36 | return AmisResult.ok(); 37 | } 38 | 39 | @PostMapping("copy") 40 | public AmisResult copy(@RequestBody 41 | @Validated(ValidGroup.CopyGroup.class) ParsingRecord parsingRecord) throws Exception { 42 | parsingRecordService.copy(parsingRecord); 43 | return AmisResult.ok(); 44 | } 45 | 46 | @PostMapping("modify") 47 | public AmisResult modify(@RequestBody 48 | @Validated(ValidGroup.UpdateGroup.class) ParsingRecord parsingRecord) throws Exception { 49 | parsingRecordService.modify(parsingRecord); 50 | return AmisResult.ok(); 51 | } 52 | 53 | @DeleteMapping("delete/{id}") 54 | public AmisResult delete(@PathVariable Long id) throws Exception { 55 | parsingRecordService.delete(id); 56 | return AmisResult.ok(); 57 | } 58 | 59 | /** 60 | * 获取ip模式 61 | * @param getIpMode 62 | * @param recordType 63 | * @return 64 | * @throws Exception 65 | */ 66 | @GetMapping("getIpModeValue") 67 | public AmisResult> getModeValue(@RequestParam Integer getIpMode,@RequestParam Integer recordType) throws Exception { 68 | List list = parsingRecordService.getModeIpValue(getIpMode,recordType); 69 | return AmisResult.ok(list); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /doc/linux-scripts/ddns4j.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JAR_VERSION="ddns-v1.6.5-RELEASE.jar" 4 | 5 | if [ -f /etc/os-release ]; then 6 | source /etc/os-release 7 | if [[ "$ID" == "centos" ]]; then 8 | echo "检测到您的系统是: centos,接下来会安装ddns4j的运行环境请稍后..." 9 | sudo yum install java-1.8.0-openjdk -y 10 | elif [[ "$ID" == "ubuntu" ]]; then 11 | echo "检测到您的系统是: ubuntu,接下来会安装ddns4j的运行环境请稍后..." 12 | sudo apt-get install openjdk-8-jdk -y 13 | elif [[ "$ID" == "opencloudos" ]]; then 14 | echo "检测到您的系统是: opencloudos,接下来会安装ddns4j的运行环境请稍后..." 15 | sudo yum install java-1.8.0-openjdk -y 16 | else 17 | echo "System type: Unknown" 18 | fi 19 | else 20 | echo "System type: Unknown" 21 | fi 22 | 23 | getAbsolutePath() { 24 | # 获取当前脚本的绝对路径 25 | SCRIPT="\$0" 26 | if [ -h "$SCRIPT" ]; then 27 | # 如果是符号链接,则获取符号链接的绝对路径 28 | while [ -h "$SCRIPT" ]; do 29 | SCRIPT=$(readlink "$SCRIPT") 30 | done 31 | fi 32 | # 返回脚本所在目录的绝对路径 33 | (cd "$(dirname "$SCRIPT")" && pwd) 34 | } 35 | 36 | install() { 37 | curPath=$(getAbsolutePath) 38 | echo $curPath 39 | # 创建一个新的.service文件 40 | sudo tee /etc/systemd/system/ddns4j.service > /dev/null << EOF 41 | [Unit] 42 | Description=ddns4j service 43 | After=network.target 44 | 45 | [Service] 46 | User=root 47 | Type=simple 48 | WorkingDirectory=$curPath 49 | ExecStart=/usr/bin/java -jar -Xmx500m -Xms500m $curPath/$JAR_VERSION 50 | ExecStop=/bin/kill -s QUIT $MAINPID 51 | Restart=always 52 | 53 | [Install] 54 | WantedBy=multi-user.target 55 | EOF 56 | 57 | # 重新加载systemd管理的服务 58 | sudo systemctl daemon-reload 59 | 60 | # 启用服务 61 | sudo systemctl enable ddns4j.service 62 | 63 | # 启动服务 64 | sudo systemctl start ddns4j.service 65 | } 66 | 67 | uninstall() { 68 | sudo systemctl stop ddns4j.service 69 | sudo systemctl disable ddns4j.service 70 | 71 | if [ -f /etc/os-release ]; then 72 | source /etc/os-release 73 | if [[ "$ID" == "centos" ]]; then 74 | echo "检测到您的系统是: centos,接下来会卸载ddns4j的运行环境请稍后..." 75 | sudo yum remove java-1.8.0-openjdk -y 76 | elif [[ "$ID" == "ubuntu" ]]; then 77 | echo "检测到您的系统是: ubuntu,接下来会卸载ddns4j的运行环境请稍后..." 78 | sudo apt-get remove openjdk-8-jdk -y 79 | elif [[ "$ID" == "opencloudos" ]]; then 80 | echo "检测到您的系统是: opencloudos,接下来会卸载ddns4j的运行环境请稍后..." 81 | sudo yum remove java-1.8.0-openjdk -y 82 | else 83 | echo "System type: Unknown" 84 | fi 85 | else 86 | echo "System type: Unknown" 87 | fi 88 | } 89 | 90 | 91 | if [ $1 = "install" ];then 92 | install 93 | echo "安装成功!请打开浏览器在页面中您的ip:10000查看管理" 94 | elif [ $1 = "uninstall" ];then 95 | uninstall 96 | echo "卸载成功!" 97 | else 98 | echo "Usage: ./ddns4j.sh [install | uninstall]" 99 | exit 1 100 | fi 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDNS4J v1.6.5 2 | 3 | **DDNS4J -- 让动态域名解析变的更简单** 4 | 5 |

6 | star 7 | fork 8 | 9 |

10 | 11 | --- 12 | 13 | ## 项目简介 14 | 15 | DDNS4J 是一个基于 SpringBoot 和 Amis 开发的完全免费开源 DDNS 服务,支持 IPv4 和 IPv6,能够帮助用户动态更新域名解析记录,从而方便地将个人服务器或家庭网络对外提供服务。 16 | 17 | ### 核心功能 18 | - 支持阿里云、腾讯云、Cloudflare 和华为云等主流 DNS 服务商 19 | - 提供 IPv4 和 IPv6 双栈支持 20 | - 自动识别公网 IP 地址并更新解析记录 21 | - 提供可视化 Web 管理界面 22 | - 支持定时任务自动更新 23 | - 提供日志记录与管理功能 24 | 25 | --- 26 | 27 | ## 使用指南 28 | 29 | ### 支持平台 30 | - Windows 31 | - Linux 32 | - Docker 33 | 34 | ### 安装步骤 35 | 36 | #### Windows 平台 37 | 1. 从 [Releases](https://gitee.com/Xsssd/ddns4j/releases) 下载 `ddns4j_setup.exe` 安装包 38 | 2. 双击安装并按照提示完成安装 39 | 40 | #### Linux 平台 41 | 1. 从 [Releases](https://gitee.com/Xsssd/ddns4j/releases) 下载 `ddns4j-linux.tar.gz` 42 | 2. 解压并授权执行: 43 | ```bash 44 | tar -zxvf ddns4j-linux.tar.gz && cd ddns4j && chmod +x ddns4j.sh 45 | ``` 46 | 3. 安装并启动: 47 | ```bash 48 | ./ddns4j.sh install 49 | ``` 50 | 4. 卸载: 51 | ```bash 52 | ./ddns4j.sh uninstall 53 | ``` 54 | 55 | #### Docker 部署 56 | **方式一:使用阿里云镜像** 57 | ```bash 58 | docker run -itd --name=ddns4j --restart=always --network=host registry.cn-hangzhou.aliyuncs.com/sssd/ddns4j:v1.6.5 59 | ``` 60 | 61 | **方式二:使用 Docker Hub 镜像** 62 | ```bash 63 | docker run -itd --name=ddns4j --restart=always --network=host topsssd/ddns4j:v1.6.5 64 | ``` 65 | 66 | 访问 `http://ip:10000` 进入管理界面 67 | 68 | --- 69 | 70 | ## 功能说明 71 | 72 | - **域名管理**:支持添加、修改、复制、删除域名解析记录 73 | - **IP 自动识别**:支持通过网卡或网络接口自动获取公网 IP 74 | - **定时更新**:可设置更新频率(每分钟、每小时、每天) 75 | - **日志查看**:记录所有解析更新操作日志 76 | - **多服务支持**:支持阿里云、腾讯云、Cloudflare、华为云等平台 77 | - **代理支持**:Cloudflare 默认开启代理模式 78 | 79 | --- 80 | 81 | ## 技术栈 82 | 83 | ### 后端 84 | - SpringBoot 85 | - MyBatisPlus 86 | - Quartz 定时任务 87 | - RestTemplate 网络请求 88 | - MySQL / H2 数据库 89 | 90 | ### 前端 91 | - Amis 可视化框架 92 | - HTML / CSS / JavaScript 93 | 94 | --- 95 | 96 | ## 项目结构 97 | 98 | ``` 99 | ├── doc/ # 文档与图片资源 100 | ├── src/ # 源码目录 101 | │ ├── main/ 102 | │ │ ├── java/ # Java 源代码 103 | │ │ └── resources/ # 配置文件与静态资源 104 | │ └── test/ # 测试代码 105 | └── pom.xml # Maven 项目配置 106 | ``` 107 | 108 | --- 109 | 110 | ## 开源许可 111 | 112 | 本项目使用 Apache-2.0 协议,详情请查看 [LICENSE](LICENSE) 文件。 113 | 114 | --- 115 | 116 | ## 联系方式 117 | 118 | 加入交流群获取更多帮助: 119 | ![QQ群二维码](doc/ddns4j交流群(一)群二维码.png) 120 | 121 | --- 122 | 123 | ## 更多信息 124 | 125 | - 项目演示地址:[https://demo.sssd.top](https://demo.sssd.top) 126 | - 项目官方地址:[https://ddns4j.sssd.top](https://ddns4j.sssd.top) 127 | - 作者博客:[https://blog.sssd.top](https://blog.sssd.top) 128 | 129 | --- 130 | 131 | > DDNS4J 是一个免费开源项目,欢迎参与贡献! -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/common/utils/PageUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.common.utils; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * @author xuyang13 10 | */ 11 | public class PageUtils implements Serializable { 12 | private static final long serialVersionUID = 1L; 13 | 14 | /** 15 | * 总记录数 16 | */ 17 | private int totalRecords; 18 | /** 19 | * 每页记录数 20 | */ 21 | private int pageSize; 22 | /** 23 | * 总页数 24 | */ 25 | private int totalPage; 26 | /** 27 | * 当前页数 28 | */ 29 | private int curPage; 30 | /** 31 | * 列表数据 32 | */ 33 | private List data; 34 | 35 | /** 36 | * 37 | */ 38 | private String sortDir; 39 | 40 | /** 41 | * 42 | */ 43 | private String sortIndx; 44 | 45 | /** 46 | * 分页 47 | */ 48 | public PageUtils(IPage page) { 49 | this.data = page.getRecords(); 50 | this.totalRecords = (int)page.getTotal(); 51 | this.pageSize = (int)page.getSize(); 52 | this.curPage = (int)page.getCurrent(); 53 | this.totalPage = (int)page.getPages(); 54 | } 55 | 56 | /** 57 | * 分页 58 | * @param data 列表数据 59 | * @param totalRecords 总记录数 60 | * @param pageSize 每页记录数 61 | * @param curPage 当前页数 62 | */ 63 | public PageUtils(List data, int totalRecords, int pageSize, int curPage) { 64 | this.data = data; 65 | this.totalRecords = totalRecords; 66 | this.pageSize = pageSize; 67 | this.curPage = curPage; 68 | this.totalPage = (int) Math.ceil((double)totalRecords/pageSize); 69 | } 70 | 71 | public int getTotalRecords() { 72 | return totalRecords; 73 | } 74 | 75 | public void setTotalRecords(int totalRecords) { 76 | this.totalRecords = totalRecords; 77 | } 78 | 79 | public int getPageSize() { 80 | return pageSize; 81 | } 82 | 83 | public void setPageSize(int pageSize) { 84 | this.pageSize = pageSize; 85 | } 86 | 87 | public int getTotalPage() { 88 | return totalPage; 89 | } 90 | 91 | public void setTotalPage(int totalPage) { 92 | this.totalPage = totalPage; 93 | } 94 | 95 | public int getCurPage() { 96 | return curPage; 97 | } 98 | 99 | public void setCurPage(int curPage) { 100 | this.curPage = curPage; 101 | } 102 | 103 | public List getData() { 104 | return data; 105 | } 106 | 107 | public void setData(List data) { 108 | this.data = data; 109 | } 110 | 111 | public String getSortDir() { 112 | return sortDir; 113 | } 114 | 115 | public void setSortDir(String sortDir) { 116 | this.sortDir = sortDir; 117 | } 118 | 119 | public String getSortIndx() { 120 | return sortIndx; 121 | } 122 | 123 | public void setSortIndx(String sortIndx) { 124 | this.sortIndx = sortIndx; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/model/entity/ParsingRecord.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import com.fasterxml.jackson.annotation.JsonFormat; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 7 | import lombok.Data; 8 | import top.sssd.ddns.common.valid.ValidGroup; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotNull; 12 | import java.io.Serializable; 13 | import java.time.LocalDateTime; 14 | 15 | /** 16 | *

17 | * 解析记录表 18 | *

19 | * 20 | * @author sssd 21 | * @since 2023-03-19 22 | */ 23 | @TableName("parsing_record") 24 | @Data 25 | public class ParsingRecord extends AmisPageEntity implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | @NotNull(groups = {ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "id不能为空") 30 | @TableId(type = IdType.ASSIGN_ID) 31 | @JsonSerialize(using = ToStringSerializer.class) 32 | private Long id; 33 | 34 | @NotNull(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "服务提供商不能为空,1 阿里云 2 腾讯云 3 cloudflare") 35 | private Integer serviceProvider; 36 | 37 | @TableField(exist = false) 38 | private String serviceProviderName; 39 | 40 | @NotBlank(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "服务提供商密钥key不能为空,1 阿里云 2 腾讯云 ") 41 | private String serviceProviderId; 42 | 43 | @NotBlank(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "服务提供商密钥value不能为空,1 阿里云 2 腾讯云,1 阿里云 2 腾讯云 3 cloudflare") 44 | private String serviceProviderSecret; 45 | 46 | @NotNull(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "解析类型不能为空,解析类型:1 AAAA 2 A") 47 | private Integer recordType; 48 | 49 | @TableField(exist = false) 50 | private String recordTypeName; 51 | 52 | private String ip; 53 | 54 | @NotNull(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "获取ip方式不能为空,获取ip方式: 1 interface 2 network 3 cmd") 55 | private Integer getIpMode; 56 | 57 | private String getIpModeValue; 58 | 59 | @NotBlank(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "域名不能为空") 60 | private String domain; 61 | 62 | @NotNull(groups = {ValidGroup.SaveGroup.class, ValidGroup.UpdateGroup.class,ValidGroup.CopyGroup.class}, message = "更新频率不能为空") 63 | private Integer updateFrequency; 64 | 65 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 66 | @TableField(fill = FieldFill.INSERT) 67 | private LocalDateTime createDate; 68 | 69 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 70 | @TableField(fill = FieldFill.INSERT_UPDATE) 71 | private LocalDateTime updateDate; 72 | 73 | @TableField(fill = FieldFill.INSERT) 74 | private Long creator; 75 | 76 | @TableField(fill = FieldFill.INSERT_UPDATE) 77 | private Long updater; 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/HuaweiDynamicDnsStrategyImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | import com.huaweicloud.sdk.dns.v2.DnsClient; 4 | import com.huaweicloud.sdk.dns.v2.model.ListRecordSetsResponse; 5 | import com.huaweicloud.sdk.dns.v2.model.ListRecordSetsWithTags; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.util.CollectionUtils; 9 | import top.sssd.ddns.common.enums.RecordTypeEnum; 10 | import top.sssd.ddns.model.entity.ParsingRecord; 11 | import top.sssd.ddns.utils.HuaweiDnsUtils; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author sssd 17 | * @careate 2023-11-13-21:56 18 | */ 19 | @Service 20 | @Slf4j 21 | public class HuaweiDynamicDnsStrategyImpl implements DynamicDnsStrategy { 22 | @Override 23 | public boolean exist(String serviceProviderId, String serviceProviderSecret, String subDomain, String recordType) throws Exception { 24 | DnsClient client = HuaweiDnsUtils.createClient(serviceProviderId, serviceProviderSecret); 25 | ListRecordSetsResponse listRecordSetsResponse = HuaweiDnsUtils.listRecordSetsByDomainWithType(client, subDomain, recordType); 26 | List set = listRecordSetsResponse.getRecordsets(); 27 | return !CollectionUtils.isEmpty(set); 28 | } 29 | 30 | @Override 31 | public void add(ParsingRecord parsingRecord, String ip) throws Exception { 32 | DnsClient client = HuaweiDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 33 | String zoneId = HuaweiDnsUtils.getPublicZoneId(client); 34 | HuaweiDnsUtils.add(client,zoneId,parsingRecord.getDomain(), RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()),ip); 35 | } 36 | 37 | @Override 38 | public void update(ParsingRecord parsingRecord, String ip, String recordId) throws Exception { 39 | DnsClient client = HuaweiDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 40 | String zoneId = HuaweiDnsUtils.getPublicZoneId(client); 41 | HuaweiDnsUtils.update(client,recordId,zoneId,parsingRecord.getDomain(),RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()),ip); 42 | } 43 | 44 | @Override 45 | public String getRecordId(ParsingRecord parsingRecord, String ip) throws Exception { 46 | DnsClient client = HuaweiDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 47 | return HuaweiDnsUtils.getRecordId(client,parsingRecord.getDomain(),RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()),ip); 48 | } 49 | 50 | @Override 51 | public void remove(ParsingRecord parsingRecord, String ip) throws Exception { 52 | DnsClient client = HuaweiDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 53 | String zoneId = HuaweiDnsUtils.getPublicZoneId(client); 54 | String recordId = getRecordId(parsingRecord, ip); 55 | HuaweiDnsUtils.delete(client,recordId,zoneId); 56 | } 57 | 58 | @Override 59 | public String getIpBySubDomainWithType(ParsingRecord parsingRecord) throws Exception { 60 | DnsClient client = HuaweiDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 61 | ListRecordSetsResponse response = HuaweiDnsUtils.listRecordSetsByDomainWithType(client, parsingRecord.getDomain(), RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType())); 62 | return response.getRecordsets().get(0).getRecords().get(0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/CloudflareDynamicDnsStrategyImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | import top.sssd.ddns.common.BizException; 7 | import top.sssd.ddns.common.enums.RecordTypeEnum; 8 | import top.sssd.ddns.model.entity.ParsingRecord; 9 | import top.sssd.ddns.utils.CloudflareUtils; 10 | 11 | /** 12 | * @author sssd 13 | * @created 2023-05-06-17:13 14 | */ 15 | @Service 16 | @Slf4j 17 | public class CloudflareDynamicDnsStrategyImpl implements DynamicDnsStrategy { 18 | 19 | @Override 20 | public boolean exist(String serviceProviderId, String serviceProviderSecret, String subDomain, String recordType) { 21 | CloudflareUtils.CloudflareQueryResponse cloudflareResponse = CloudflareUtils.getSubDomainParseList(serviceProviderId, serviceProviderSecret, subDomain, recordType); 22 | return !cloudflareResponse.getResult().isEmpty(); 23 | } 24 | 25 | @Override 26 | public void add(ParsingRecord parsingRecord, String ip) throws JsonProcessingException { 27 | CloudflareUtils.CloudflareResponse cloudflareResponse = 28 | CloudflareUtils.add(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret(), parsingRecord.getDomain(), RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), ip); 29 | if (Boolean.FALSE.equals(cloudflareResponse.getSuccess())) { 30 | log.error("域名添加失败:{}", parsingRecord); 31 | throw new BizException("域名添加失败"); 32 | } 33 | 34 | } 35 | 36 | @Override 37 | public void update(ParsingRecord parsingRecord, String ip, String recordId) { 38 | CloudflareUtils.CloudflareResponse cloudflareResponse = CloudflareUtils.update(parsingRecord.getServiceProviderId(), 39 | parsingRecord.getServiceProviderSecret(), 40 | parsingRecord.getDomain(), 41 | RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), ip,recordId); 42 | if (Boolean.FALSE.equals(cloudflareResponse.getSuccess())) { 43 | log.error("域名更新失败:{}", parsingRecord); 44 | throw new BizException("域名更新失败"); 45 | } 46 | } 47 | 48 | @Override 49 | public String getRecordId(ParsingRecord parsingRecord, String ip) { 50 | return CloudflareUtils 51 | .getId(parsingRecord.getServiceProviderId() 52 | , parsingRecord.getServiceProviderSecret() 53 | , parsingRecord.getDomain() 54 | , RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), ip); 55 | } 56 | 57 | @Override 58 | public void remove(ParsingRecord parsingRecord, String ip) { 59 | CloudflareUtils.CloudflareResponse cloudflareResponse = CloudflareUtils.delete(parsingRecord.getServiceProviderId() 60 | , parsingRecord.getServiceProviderSecret() 61 | , parsingRecord.getDomain() 62 | , RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), ip); 63 | if (Boolean.FALSE.equals(cloudflareResponse.getSuccess())) { 64 | log.error("域名删除失败:{}", parsingRecord); 65 | throw new BizException("域名删除失败"); 66 | } 67 | } 68 | 69 | @Override 70 | public String getIpBySubDomainWithType(ParsingRecord parsingRecord) { 71 | return CloudflareUtils.getIpBySubDomainWithType(parsingRecord.getServiceProviderId() 72 | , parsingRecord.getServiceProviderSecret() 73 | , parsingRecord.getDomain() 74 | , RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType())); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/utils/TencentCloudAPITC3Singer.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import javax.crypto.Mac; 6 | import javax.crypto.spec.SecretKeySpec; 7 | import javax.xml.bind.DatatypeConverter; 8 | import java.nio.charset.Charset; 9 | import java.nio.charset.StandardCharsets; 10 | import java.security.MessageDigest; 11 | import java.time.Instant; 12 | import java.time.LocalDate; 13 | import java.time.format.DateTimeFormatter; 14 | import java.util.TreeMap; 15 | 16 | /** 17 | * @author sssd 18 | * @careate 2023-12-18-15:03 19 | */ 20 | @Slf4j 21 | public class TencentCloudAPITC3Singer { 22 | private TencentCloudAPITC3Singer(){} 23 | private static final Charset UTF8 = StandardCharsets.UTF_8; 24 | private static final String CT_JSON = "application/json; charset=utf-8"; 25 | private static final String SERVICE = "dnspod"; 26 | public static final String HOST = "dnspod.tencentcloudapi.com"; 27 | private static final String VERSION = "2021-03-23"; 28 | private static final String ALGORITHM = "TC3-HMAC-SHA256"; 29 | 30 | private static byte[] hmac256(byte[] key, String msg) throws Exception { 31 | Mac mac = Mac.getInstance("HmacSHA256"); 32 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); 33 | mac.init(secretKeySpec); 34 | return mac.doFinal(msg.getBytes(UTF8)); 35 | } 36 | 37 | private static String sha256Hex(String s) throws Exception { 38 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 39 | byte[] d = md.digest(s.getBytes(UTF8)); 40 | return DatatypeConverter.printHexBinary(d).toLowerCase(); 41 | } 42 | 43 | public static TreeMap buildSignRequestHeaderWithBody(String secretId, String secretKey, String action, String jsonBody) throws Exception { 44 | String timestamp = String.valueOf(Instant.now().getEpochSecond()); 45 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); 46 | // log.info("************* TencentCloud: 步骤 1:拼接规范请求串 *************"); 47 | String httpRequestMethod = "POST"; 48 | String canonicalUri = "/"; 49 | String canonicalQueryString = ""; 50 | String canonicalHeaders = "content-type:application/json; charset=utf-8\n" 51 | + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; 52 | String signedHeaders = "content-type;host;x-tc-action"; 53 | String hashedRequestPayload = sha256Hex(jsonBody); 54 | String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" 55 | + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload; 56 | // log.info("************* TencentCloud: 步骤 2:拼接待签名字符串 *************"); 57 | String credentialScope = date + "/" + SERVICE + "/" + "tc3_request"; 58 | String hashedCanonicalRequest = sha256Hex(canonicalRequest); 59 | String stringToSign = ALGORITHM + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; 60 | // log.info("************* TencentCloud: 步骤 3:计算签名 *************"); 61 | byte[] secretDate = hmac256(("TC3" + secretKey).getBytes(UTF8), date); 62 | byte[] secretService = hmac256(secretDate, SERVICE); 63 | byte[] secretSigning = hmac256(secretService, "tc3_request"); 64 | String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase(); 65 | // log.info("************* TencentCloud: 步骤 4:拼接 Authorization *************"); 66 | String authorization = ALGORITHM + " " + "Credential=" + secretId + "/" + credentialScope + ", " 67 | + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; 68 | // log.info("************* TencentCloud: 步骤 5:拼接 请求头 *************"); 69 | TreeMap headers = new TreeMap<>(); 70 | headers.put("Authorization", authorization); 71 | headers.put("Content-Type", CT_JSON); 72 | headers.put("Host", HOST); 73 | headers.put("X-TC-Action", action); 74 | headers.put("X-TC-Timestamp", timestamp); 75 | headers.put("X-TC-Version", VERSION); 76 | return headers; 77 | } 78 | 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/AliDynamicDnsStrategyImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | import com.aliyun.alidns20150109.Client; 4 | import com.aliyun.alidns20150109.models.DescribeSubDomainRecordsResponse; 5 | import com.aliyun.alidns20150109.models.DescribeSubDomainRecordsResponseBody; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.util.StringUtils; 10 | import top.sssd.ddns.common.BizException; 11 | import top.sssd.ddns.common.enums.RecordTypeEnum; 12 | import top.sssd.ddns.model.entity.ParsingRecord; 13 | import top.sssd.ddns.utils.AliDnsUtils; 14 | 15 | import static top.sssd.ddns.common.utils.DoMainUtil.findNthOccurrence; 16 | import static top.sssd.ddns.common.utils.DoMainUtil.firstLevel; 17 | 18 | /** 19 | * @author sssd 20 | * @created 2023-03-20-13:41 21 | */ 22 | @Service 23 | @Slf4j 24 | public class AliDynamicDnsStrategyImpl implements DynamicDnsStrategy { 25 | @Override 26 | public boolean exist(String serviceProviderId, String serviceProviderSecret, String subDomain, String recordType) throws Exception { 27 | Client client = AliDnsUtils.createClient(serviceProviderId, serviceProviderSecret); 28 | DescribeSubDomainRecordsResponse response = null; 29 | try { 30 | response = AliDnsUtils.getSubDomainParseList(client, subDomain, recordType); 31 | } catch (Exception e) { 32 | return false; 33 | } 34 | if (response.statusCode != HttpStatus.OK.value()) { 35 | log.error("调用阿里云DNS解析失败,请检查传入的serviceProviderId,serviceProviderSecret,域名是否正确"); 36 | throw new BizException("调用阿里云DNS解析失败,请检查传入的serviceProviderId,serviceProviderSecret,域名是否正确"); 37 | } 38 | DescribeSubDomainRecordsResponseBody body = response.getBody(); 39 | return body.getTotalCount() > 0 ; 40 | } 41 | 42 | @Override 43 | public void add(ParsingRecord parsingRecord, String ip) throws Exception { 44 | //call dns api 45 | Client client = AliDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 46 | String subDoMain = parsingRecord.getDomain(); 47 | String domain = null; 48 | String rr = null; 49 | if (firstLevel(subDoMain)) { 50 | domain = subDoMain; 51 | rr = "@"; 52 | } else { 53 | domain = subDoMain.substring(findNthOccurrence(subDoMain, ".", 1) + 1); 54 | rr = subDoMain.substring(0, findNthOccurrence(subDoMain, ".", 1)); 55 | } 56 | AliDnsUtils.add(client, domain, rr, RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), ip); 57 | } 58 | 59 | 60 | @Override 61 | public void update(ParsingRecord parsingRecord, String ip,String recordId) throws Exception { 62 | //call dns api 63 | Client client = AliDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 64 | 65 | String subDoMain = parsingRecord.getDomain(); 66 | 67 | String rr = null; 68 | if (firstLevel(subDoMain)) { 69 | rr = "@"; 70 | } else { 71 | rr = subDoMain.substring(0, findNthOccurrence(subDoMain, ".", 1)); 72 | } 73 | String recordTypeName = RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()); 74 | 75 | AliDnsUtils.update(client, recordId, rr, recordTypeName, ip); 76 | } 77 | 78 | @Override 79 | public String getRecordId(ParsingRecord parsingRecord, String ip) throws Exception { 80 | //call dns api 81 | Client client = AliDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 82 | 83 | String subDoMain = parsingRecord.getDomain(); 84 | 85 | String recordTypeName = RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()); 86 | 87 | String recordId = AliDnsUtils.getDomainRecordId(client, subDoMain, recordTypeName, ip); 88 | if (!StringUtils.hasText(recordId)) { 89 | throw new BizException("没有该域名对应的解析记录"); 90 | } 91 | return recordId; 92 | } 93 | 94 | @Override 95 | public void remove(ParsingRecord parsingRecord, String ip) throws Exception { 96 | Client client = AliDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 97 | String recordTypeName = RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()); 98 | String recordId = AliDnsUtils.getDomainRecordId(client, parsingRecord.getDomain(), recordTypeName, ip); 99 | 100 | AliDnsUtils.delete(client, recordId); 101 | } 102 | 103 | @Override 104 | public String getIpBySubDomainWithType(ParsingRecord parsingRecord) throws Exception { 105 | Client client = AliDnsUtils.createClient(parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 106 | return AliDnsUtils.getIpBySubDomainWithType(client, parsingRecord.getDomain(), RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType())); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/utils/HuaweiDnsUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.utils; 2 | 3 | import com.huaweicloud.sdk.core.auth.BasicCredentials; 4 | import com.huaweicloud.sdk.core.auth.ICredential; 5 | import com.huaweicloud.sdk.dns.v2.DnsClient; 6 | import com.huaweicloud.sdk.dns.v2.model.*; 7 | import com.huaweicloud.sdk.dns.v2.region.DnsRegion; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * @author sssd 15 | * @create 2023-11-13-21:04 16 | */ 17 | @Slf4j 18 | public class HuaweiDnsUtils { 19 | 20 | /** 21 | * 查询节点:中国香港 22 | */ 23 | private static String dnsRegion = "ap-southeast-1"; 24 | /** 25 | * 查询模式:精确 26 | */ 27 | private static String searchMode = "equal"; 28 | 29 | private HuaweiDnsUtils() { 30 | } 31 | 32 | public static DnsClient createClient(String secretId, String secretKey) { 33 | ICredential auth = new BasicCredentials() 34 | .withAk(secretId) 35 | .withSk(secretKey); 36 | return DnsClient.newBuilder() 37 | .withCredential(auth) 38 | .withRegion(DnsRegion.valueOf(dnsRegion)) 39 | .build(); 40 | } 41 | 42 | public static String getPublicZoneId(DnsClient dnsClient) { 43 | ListPublicZonesRequest request = new ListPublicZonesRequest(); 44 | ListPublicZonesResponse response = dnsClient.listPublicZones(request); 45 | return response.getZones().get(0).getId(); 46 | } 47 | 48 | public static ListRecordSetsResponse listRecordSets(DnsClient dnsClient) { 49 | ListRecordSetsRequest request = new ListRecordSetsRequest(); 50 | return dnsClient.listRecordSets(request); 51 | } 52 | 53 | public static ListRecordSetsResponse listRecordSetsByType(DnsClient dnsClient, String recordType) { 54 | ListRecordSetsRequest request = new ListRecordSetsRequest(); 55 | request.setSearchMode(searchMode); 56 | request.setType(recordType); 57 | return dnsClient.listRecordSets(request); 58 | } 59 | 60 | public static ListRecordSetsResponse listRecordSetsByDomain(DnsClient dnsClient, String domain) { 61 | ListRecordSetsRequest request = new ListRecordSetsRequest(); 62 | request.setSearchMode(searchMode); 63 | request.setName(domain); 64 | return dnsClient.listRecordSets(request); 65 | } 66 | 67 | public static ListRecordSetsResponse listRecordSetsByDomainWithType(DnsClient dnsClient, String domain, String recordType) { 68 | ListRecordSetsRequest request = new ListRecordSetsRequest(); 69 | request.setSearchMode(searchMode); 70 | request.setName(domain); 71 | request.setType(recordType); 72 | return dnsClient.listRecordSets(request); 73 | } 74 | 75 | public static ListRecordSetsResponse listRecordSetsByDomainWithTypeAndIp(DnsClient dnsClient, String domain, String recordType, String ip) { 76 | ListRecordSetsRequest request = new ListRecordSetsRequest(); 77 | request.setSearchMode(searchMode); 78 | request.setName(domain); 79 | request.setType(recordType); 80 | request.setRecords(ip); 81 | return dnsClient.listRecordSets(request); 82 | } 83 | 84 | public static String getRecordId(DnsClient dnsClient, String domain, String recordType, String ip) { 85 | return listRecordSetsByDomainWithTypeAndIp(dnsClient, domain, recordType, ip).getRecordsets().get(0).getId(); 86 | } 87 | 88 | public static CreateRecordSetResponse add(DnsClient dnsClient, String zoneId, String domain, String recordType, String ip) { 89 | CreateRecordSetRequest request = new CreateRecordSetRequest(); 90 | request.withZoneId(zoneId); 91 | CreateRecordSetRequestBody body = new CreateRecordSetRequestBody(); 92 | body.setName(domain); 93 | body.setType(recordType); 94 | List ips = new ArrayList<>(); 95 | ips.add(ip); 96 | body.withRecords(ips); 97 | request.withBody(body); 98 | return dnsClient.createRecordSet(request); 99 | } 100 | 101 | public static UpdateRecordSetResponse update(DnsClient dnsClient, String recordId, String zoneId, String domain, String recordType, String ip) { 102 | UpdateRecordSetRequest request = new UpdateRecordSetRequest(); 103 | request.withZoneId(zoneId); 104 | request.withRecordsetId(recordId); 105 | UpdateRecordSetReq body = new UpdateRecordSetReq(); 106 | List ips = new ArrayList<>(); 107 | ips.add(ip); 108 | body.withRecords(ips); 109 | body.withType(recordType); 110 | body.withName(domain); 111 | request.withBody(body); 112 | return dnsClient.updateRecordSet(request); 113 | } 114 | 115 | public static DeleteRecordSetResponse delete(DnsClient dnsClient, String recordId, String zoneId) { 116 | DeleteRecordSetRequest request = new DeleteRecordSetRequest(); 117 | request.withZoneId(zoneId); 118 | request.withRecordsetId(recordId); 119 | return dnsClient.deleteRecordSet(request); 120 | } 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | top.sssd 7 | ddns 8 | v1.6.5-RELEASE 9 | ddns4j 10 | 使用java实现动态域名解析 11 | jar 12 | 13 | 14 | 15 | 1.8 16 | UTF-8 17 | UTF-8 18 | 2.7.16 19 | 3.5.1 20 | 8.0.23 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-dependencies 28 | ${spring-boot.version} 29 | pom 30 | import 31 | 32 | 33 | 34 | com.baomidou 35 | mybatis-plus-boot-starter 36 | ${mybatis-plus.version} 37 | 38 | 39 | 40 | mysql 41 | mysql-connector-java 42 | ${mysql.version} 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-validation 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-logging 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-quartz 63 | 64 | 65 | 66 | com.baomidou 67 | mybatis-plus-boot-starter 68 | 69 | 70 | 71 | mysql 72 | mysql-connector-java 73 | 74 | 75 | 76 | com.h2database 77 | h2 78 | runtime 79 | 80 | 81 | 82 | org.projectlombok 83 | lombok 84 | true 85 | 86 | 87 | 88 | 89 | com.aliyun 90 | alidns20150109 91 | 3.0.1 92 | 93 | 94 | com.huaweicloud.sdk 95 | huaweicloud-sdk-dns 96 | 3.1.65 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-compiler-plugin 106 | 3.8.1 107 | 108 | 1.8 109 | 1.8 110 | UTF-8 111 | 112 | 113 | 114 | org.springframework.boot 115 | spring-boot-maven-plugin 116 | 2.7.16 117 | 118 | true 119 | ZIP 120 | top.sssd.ddns.DynamicDnsApplication 121 | 122 | 123 | 124 | 125 | repackage 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/strategy/TencentDynamicDnsStrategyImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.strategy; 2 | 3 | 4 | import org.springframework.stereotype.Service; 5 | import top.sssd.ddns.common.BizException; 6 | import top.sssd.ddns.common.enums.RecordTypeEnum; 7 | import top.sssd.ddns.model.entity.ParsingRecord; 8 | import top.sssd.ddns.utils.TencentDnsUtils; 9 | 10 | import java.util.Objects; 11 | 12 | import static top.sssd.ddns.common.utils.DoMainUtil.spiltDomain; 13 | 14 | /** 15 | * @author sssd 16 | * @created 2023-05-06-17:11 17 | */ 18 | @Service 19 | public class TencentDynamicDnsStrategyImpl implements DynamicDnsStrategy { 20 | 21 | @Override 22 | public boolean exist(String serviceProviderId, String serviceProviderSecret, String domain, String recordType) throws Exception { 23 | String[] domains = spiltDomain(domain); 24 | TencentDnsUtils.ListRecordResponse listRecordResponse = TencentDnsUtils.getRecordList(domains[0], domains[1], recordType, serviceProviderId, serviceProviderSecret); 25 | 26 | if(Objects.isNull(listRecordResponse)){ 27 | throw new BizException("TencentCloud 查询列表记录失败 没有来自腾讯云的响应"); 28 | } 29 | TencentDnsUtils.ListResponse listResponse = listRecordResponse.getResponse(); 30 | if(Objects.isNull(listResponse)){ 31 | throw new BizException("TencentCloud 查询列表记录失败 腾讯云的列表记录响应对象为空"); 32 | } 33 | return !(listResponse.getRecordList().isEmpty()); 34 | } 35 | 36 | @Override 37 | public void add(ParsingRecord parsingRecord, String ip) throws Exception { 38 | String domain = parsingRecord.getDomain(); 39 | String[] domains = spiltDomain(domain); 40 | TencentDnsUtils.CreateRecordResponse recordResponse = TencentDnsUtils.createRecord(domains[0], domains[1], RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret(), ip); 41 | if(Objects.isNull(recordResponse)){ 42 | throw new BizException("TencentCloud 添加记录失败 没有来自腾讯云的响应"); 43 | } 44 | TencentDnsUtils.CreateResponse createResponse = recordResponse.getResponse(); 45 | if(Objects.isNull(createResponse)){ 46 | throw new BizException("TencentCloud 添加记录失败 腾讯云的添加响应对象为空"); 47 | } 48 | TencentDnsUtils.Error error = createResponse.getError(); 49 | if(Objects.nonNull(error)){ 50 | throw new BizException("TencentCloud 添加记录失败"+error.getMessage()); 51 | } 52 | } 53 | 54 | @Override 55 | public void update(ParsingRecord parsingRecord, String ip, String recordId) throws Exception { 56 | String domain = parsingRecord.getDomain(); 57 | String[] domains = spiltDomain(domain); 58 | TencentDnsUtils.UpdateRecordResponse updateRecordResponse = TencentDnsUtils.updateRecord(domains[0], domains[1], RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret(), ip, Integer.parseInt(recordId)); 59 | if(Objects.isNull(updateRecordResponse)){ 60 | throw new BizException("TencentCloud 更新记录失败 没有来自腾讯云的响应"); 61 | } 62 | TencentDnsUtils.UpdateResponse updateResponse = updateRecordResponse.getResponse(); 63 | if(Objects.isNull(updateResponse)){ 64 | throw new BizException("TencentCloud 更新记录失败 腾讯云的更新响应对象为空"); 65 | } 66 | TencentDnsUtils.Error error = updateResponse.getError(); 67 | if(Objects.nonNull(error)){ 68 | throw new BizException("TencentCloud 更新记录失败"+error.getMessage()); 69 | } 70 | } 71 | 72 | @Override 73 | public String getRecordId(ParsingRecord parsingRecord, String ip) throws Exception { 74 | String domain = parsingRecord.getDomain(); 75 | String[] domains = spiltDomain(domain); 76 | return TencentDnsUtils.getRecordId(domains[0], domains[1], RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 77 | } 78 | 79 | @Override 80 | public void remove(ParsingRecord parsingRecord, String ip) throws Exception { 81 | String domain = parsingRecord.getDomain(); 82 | String resultDomain = domain.substring(domain.indexOf('.') + 1); 83 | String recordId = getRecordId(parsingRecord, ip); 84 | TencentDnsUtils.DeleteRecordResponse deleteRecordResponse = TencentDnsUtils.deleteRecord(resultDomain, parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret(), Integer.parseInt(recordId)); 85 | if(Objects.isNull(deleteRecordResponse)){ 86 | throw new BizException("TencentCloud 删除记录失败 没有来自腾讯云的响应"); 87 | } 88 | TencentDnsUtils.DeleteResponse deleteResponse = deleteRecordResponse.getResponse(); 89 | if(Objects.isNull(deleteResponse)){ 90 | throw new BizException("TencentCloud 删除记录失败 腾讯云的删除响应对象为空"); 91 | } 92 | TencentDnsUtils.Error error = deleteResponse.getError(); 93 | if(Objects.nonNull(error)){ 94 | throw new BizException("TencentCloud 删除记录失败"+error.getMessage()); 95 | } 96 | } 97 | 98 | @Override 99 | public String getIpBySubDomainWithType(ParsingRecord parsingRecord) throws Exception { 100 | String domain = parsingRecord.getDomain(); 101 | String[] domains = spiltDomain(domain); 102 | return TencentDnsUtils.getIpBySubDomainWithType(domains[0], domains[1], RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()), parsingRecord.getServiceProviderId(), parsingRecord.getServiceProviderSecret()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/resources/sql/ddns4j_h2.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS changed_log ( 2 | id bigint NOT NULL AUTO_INCREMENT, 3 | content varchar(255) NOT NULL, 4 | insert_date datetime NOT NULL, 5 | PRIMARY KEY (id) 6 | ); 7 | 8 | 9 | CREATE TABLE IF NOT EXISTS job_task ( 10 | id INT NOT NULL AUTO_INCREMENT, 11 | name VARCHAR(255), 12 | group_name VARCHAR(255), 13 | cron_expression VARCHAR(255), 14 | class_name VARCHAR(255), 15 | description VARCHAR(255), 16 | status INT DEFAULT 0, 17 | PRIMARY KEY (id) 18 | ); 19 | 20 | CREATE TABLE IF NOT EXISTS parsing_record ( 21 | id BIGINT NOT NULL AUTO_INCREMENT, 22 | service_provider INT NOT NULL, 23 | service_provider_id VARCHAR(64), 24 | service_provider_secret VARCHAR(64), 25 | record_type INT NOT NULL, 26 | get_ip_mode INT NOT NULL, 27 | get_ip_mode_value VARCHAR(255) NOT NULL, 28 | ip VARCHAR(255) NOT NULL, 29 | domain VARCHAR(100) NOT NULL, 30 | update_frequency INT NOT NULL, 31 | create_date DATETIME, 32 | update_date DATETIME, 33 | creator BIGINT, 34 | updater BIGINT, 35 | PRIMARY KEY (id) 36 | ); 37 | 38 | -- quartz自带表结构 39 | CREATE TABLE IF NOT EXISTS QRTZ_JOB_DETAILS( 40 | SCHED_NAME VARCHAR(120) NOT NULL, 41 | JOB_NAME VARCHAR(200) NOT NULL, 42 | JOB_GROUP VARCHAR(200) NOT NULL, 43 | DESCRIPTION VARCHAR(250) NULL, 44 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 45 | IS_DURABLE VARCHAR(5) NOT NULL, 46 | IS_NONCONCURRENT VARCHAR(5) NOT NULL, 47 | IS_UPDATE_DATA VARCHAR(5) NOT NULL, 48 | REQUESTS_RECOVERY VARCHAR(5) NOT NULL, 49 | JOB_DATA BLOB NULL, 50 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 51 | ); 52 | 53 | CREATE TABLE IF NOT EXISTS QRTZ_TRIGGERS ( 54 | SCHED_NAME VARCHAR(120) NOT NULL, 55 | TRIGGER_NAME VARCHAR(200) NOT NULL, 56 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 57 | JOB_NAME VARCHAR(200) NOT NULL, 58 | JOB_GROUP VARCHAR(200) NOT NULL, 59 | DESCRIPTION VARCHAR(250) NULL, 60 | NEXT_FIRE_TIME BIGINT NULL, 61 | PREV_FIRE_TIME BIGINT NULL, 62 | PRIORITY INTEGER NULL, 63 | TRIGGER_STATE VARCHAR(16) NOT NULL, 64 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 65 | START_TIME BIGINT NOT NULL, 66 | END_TIME BIGINT NULL, 67 | CALENDAR_NAME VARCHAR(200) NULL, 68 | MISFIRE_INSTR SMALLINT NULL, 69 | JOB_DATA BLOB NULL, 70 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 71 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 72 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) 73 | ); 74 | 75 | CREATE TABLE IF NOT EXISTS QRTZ_SIMPLE_TRIGGERS ( 76 | SCHED_NAME VARCHAR(120) NOT NULL, 77 | TRIGGER_NAME VARCHAR(200) NOT NULL, 78 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 79 | REPEAT_COUNT BIGINT NOT NULL, 80 | REPEAT_INTERVAL BIGINT NOT NULL, 81 | TIMES_TRIGGERED BIGINT NOT NULL, 82 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 83 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 84 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 85 | ); 86 | 87 | CREATE TABLE IF NOT EXISTS QRTZ_CRON_TRIGGERS ( 88 | SCHED_NAME VARCHAR(120) NOT NULL, 89 | TRIGGER_NAME VARCHAR(200) NOT NULL, 90 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 91 | CRON_EXPRESSION VARCHAR(120) NOT NULL, 92 | TIME_ZONE_ID VARCHAR(80), 93 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 94 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 95 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 96 | ); 97 | 98 | CREATE TABLE IF NOT EXISTS QRTZ_SIMPROP_TRIGGERS 99 | ( 100 | SCHED_NAME VARCHAR(120) NOT NULL, 101 | TRIGGER_NAME VARCHAR(200) NOT NULL, 102 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 103 | STR_PROP_1 VARCHAR(512) NULL, 104 | STR_PROP_2 VARCHAR(512) NULL, 105 | STR_PROP_3 VARCHAR(512) NULL, 106 | INT_PROP_1 INT NULL, 107 | INT_PROP_2 INT NULL, 108 | LONG_PROP_1 BIGINT NULL, 109 | LONG_PROP_2 BIGINT NULL, 110 | DEC_PROP_1 DECIMAL(13,4) NULL, 111 | DEC_PROP_2 DECIMAL(13,4) NULL, 112 | BOOL_PROP_1 VARCHAR(5) NULL, 113 | BOOL_PROP_2 VARCHAR(5) NULL, 114 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 115 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 116 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 117 | ); 118 | 119 | CREATE TABLE IF NOT EXISTS QRTZ_BLOB_TRIGGERS ( 120 | SCHED_NAME VARCHAR(120) NOT NULL, 121 | TRIGGER_NAME VARCHAR(200) NOT NULL, 122 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 123 | BLOB_DATA BLOB NULL, 124 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 125 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 126 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 127 | ); 128 | 129 | CREATE TABLE IF NOT EXISTS QRTZ_CALENDARS ( 130 | SCHED_NAME VARCHAR(120) NOT NULL, 131 | CALENDAR_NAME VARCHAR(200) NOT NULL, 132 | CALENDAR BLOB NOT NULL, 133 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) 134 | ); 135 | 136 | CREATE TABLE IF NOT EXISTS QRTZ_PAUSED_TRIGGER_GRPS ( 137 | SCHED_NAME VARCHAR(120) NOT NULL, 138 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 139 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) 140 | ); 141 | 142 | CREATE TABLE IF NOT EXISTS QRTZ_FIRED_TRIGGERS ( 143 | SCHED_NAME VARCHAR(120) NOT NULL, 144 | ENTRY_ID VARCHAR(95) NOT NULL, 145 | TRIGGER_NAME VARCHAR(200) NOT NULL, 146 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 147 | INSTANCE_NAME VARCHAR(200) NOT NULL, 148 | FIRED_TIME BIGINT NOT NULL, 149 | SCHED_TIME BIGINT NOT NULL, 150 | PRIORITY INTEGER NOT NULL, 151 | STATE VARCHAR(16) NOT NULL, 152 | JOB_NAME VARCHAR(200) NULL, 153 | JOB_GROUP VARCHAR(200) NULL, 154 | IS_NONCONCURRENT VARCHAR(5) NULL, 155 | REQUESTS_RECOVERY VARCHAR(5) NULL, 156 | PRIMARY KEY (SCHED_NAME,ENTRY_ID) 157 | ); 158 | 159 | CREATE TABLE IF NOT EXISTS QRTZ_SCHEDULER_STATE ( 160 | SCHED_NAME VARCHAR(120) NOT NULL, 161 | INSTANCE_NAME VARCHAR(200) NOT NULL, 162 | LAST_CHECKIN_TIME BIGINT NOT NULL, 163 | CHECKIN_INTERVAL BIGINT NOT NULL, 164 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) 165 | ); 166 | 167 | CREATE TABLE IF NOT EXISTS QRTZ_LOCKS ( 168 | SCHED_NAME VARCHAR(120) NOT NULL, 169 | LOCK_NAME VARCHAR(40) NOT NULL, 170 | PRIMARY KEY (SCHED_NAME,LOCK_NAME) 171 | ); 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/utils/AliDnsUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.utils; 2 | 3 | import com.aliyun.alidns20150109.models.*; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.HttpStatus; 6 | import top.sssd.ddns.common.BizException; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * @author sssd 12 | * @update 2022-12-17 23:40 13 | */ 14 | @Slf4j 15 | public class AliDnsUtils { 16 | 17 | private static final String ENDPOINT = "alidns.cn-hangzhou.aliyuncs.com"; 18 | 19 | private AliDnsUtils() { 20 | } 21 | 22 | /** 23 | * 使用AK&SK初始化账号Client 24 | * 25 | * @param accessKeyId 26 | * @param accessKeySecret 27 | * @return Client 28 | * @throws Exception 29 | */ 30 | public static com.aliyun.alidns20150109.Client createClient(String accessKeyId, String accessKeySecret) throws Exception { 31 | com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config() 32 | // 必填,您的 AccessKey ID 33 | .setAccessKeyId(accessKeyId) 34 | // 必填,您的 AccessKey Secret 35 | .setAccessKeySecret(accessKeySecret); 36 | config.endpoint = ENDPOINT; 37 | return new com.aliyun.alidns20150109.Client(config); 38 | } 39 | 40 | /** 41 | * 获取子域名的所有解析记录列表 42 | */ 43 | public static DescribeSubDomainRecordsResponse getSubDomainParseList(com.aliyun.alidns20150109.Client client, String subDomain, String recordType) throws Exception { 44 | DescribeSubDomainRecordsRequest describeSubDomainRecordsRequest = new DescribeSubDomainRecordsRequest() 45 | .setSubDomain(subDomain) 46 | .setType(recordType); 47 | return client.describeSubDomainRecords(describeSubDomainRecordsRequest); 48 | } 49 | 50 | /** 51 | * 获取主域名的所有解析记录列表 52 | * 53 | * @param client 54 | * @param domain 55 | * @return 56 | */ 57 | public static DescribeDomainRecordsResponse getParseList(com.aliyun.alidns20150109.Client client, String domain) throws Exception { 58 | DescribeDomainRecordsRequest describeDomainRecordsRequest = new DescribeDomainRecordsRequest() 59 | .setDomainName(domain); 60 | return client.describeDomainRecords(describeDomainRecordsRequest); 61 | } 62 | 63 | /** 64 | * 添加一条解析记录 65 | * 66 | * @param client 67 | * @param domain 68 | * @param Rr 69 | * @param recordType 70 | * @param ip 71 | * @return 72 | */ 73 | public static AddDomainRecordResponse add(com.aliyun.alidns20150109.Client client, String domain, String Rr, String recordType, String ip) throws Exception { 74 | AddDomainRecordRequest addDomainRecordRequest = new AddDomainRecordRequest() 75 | .setDomainName(domain) 76 | .setRR(Rr) 77 | .setType(recordType) 78 | .setValue(ip); 79 | return client.addDomainRecord(addDomainRecordRequest); 80 | } 81 | 82 | /** 83 | * 修改解析记录 84 | * 85 | * @param client 86 | * @param recordId 87 | * @param Rr 88 | * @param recordType 89 | * @param ip 90 | * @return 91 | */ 92 | public static UpdateDomainRecordResponse update(com.aliyun.alidns20150109.Client client, String recordId, String Rr, String recordType, String ip) throws Exception { 93 | UpdateDomainRecordRequest updateDomainRecordRequest = new UpdateDomainRecordRequest() 94 | .setRecordId(recordId) 95 | .setRR(Rr) 96 | .setType(recordType) 97 | .setValue(ip); 98 | return client.updateDomainRecord(updateDomainRecordRequest); 99 | } 100 | 101 | 102 | /** 103 | * 删除解析记录 104 | * @param client 105 | * @param recordId 106 | * @return 107 | */ 108 | public static DeleteDomainRecordResponse delete(com.aliyun.alidns20150109.Client client,String recordId) throws Exception { 109 | DeleteDomainRecordRequest deleteDomainRecordRequest = new DeleteDomainRecordRequest() 110 | .setRecordId(recordId); 111 | return client.deleteDomainRecord(deleteDomainRecordRequest); 112 | } 113 | 114 | 115 | /** 116 | * 查询并返回记录ID 117 | * 118 | * @param client 119 | * @param subDomain 120 | * @param recordType 121 | * @param ip 122 | * @return 123 | */ 124 | public static String getDomainRecordId(com.aliyun.alidns20150109.Client client, String subDomain, String recordType, String ip) throws Exception { 125 | DescribeSubDomainRecordsResponse response = getSubDomainParseList(client, subDomain, recordType); 126 | if (response.getStatusCode() != HttpStatus.OK.value()) { 127 | throw new BizException("查询并返回记录ID时,调用阿里云DNS解析失败,请检查传入的serviceProviderId,serviceProviderSecret,域名是否正确"); 128 | } 129 | 130 | for (DescribeSubDomainRecordsResponseBody.DescribeSubDomainRecordsResponseBodyDomainRecordsRecord domainRecordsRecord : response.getBody().getDomainRecords().getRecord()) { 131 | if (ip.equals(domainRecordsRecord.getValue())) { 132 | return domainRecordsRecord.getRecordId(); 133 | } 134 | } 135 | return null; 136 | } 137 | 138 | 139 | /** 140 | * 根据域名和解析类型查询ip 141 | * @param client 142 | * @param subDomain 143 | * @param recordType 144 | * @return 145 | */ 146 | public static String getIpBySubDomainWithType(com.aliyun.alidns20150109.Client client,String subDomain, String recordType) throws Exception { 147 | DescribeSubDomainRecordsRequest describeSubDomainRecordsRequest = new DescribeSubDomainRecordsRequest() 148 | .setSubDomain(subDomain) 149 | .setType(recordType); 150 | DescribeSubDomainRecordsResponse response = client.describeSubDomainRecords(describeSubDomainRecordsRequest); 151 | List records = response.getBody().getDomainRecords().getRecord(); 152 | return records.get(0).getValue(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/impl/JobTaskServiceImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.quartz.*; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import top.sssd.ddns.common.BizException; 9 | import top.sssd.ddns.mapper.JobTaskMapper; 10 | import top.sssd.ddns.model.entity.JobTask; 11 | import top.sssd.ddns.service.IJobTaskService; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author sssd 17 | * @created 2023-05-02-11:06 18 | */ 19 | @Service 20 | @Slf4j 21 | public class JobTaskServiceImpl extends ServiceImpl implements IJobTaskService { 22 | @Autowired 23 | private Scheduler scheduler; 24 | 25 | @Override 26 | public boolean addJobTask(JobTask jobTask) { 27 | boolean result = this.save(jobTask); 28 | if (result) { 29 | JobKey jobKey = new JobKey(jobTask.getName(), jobTask.getGroupName()); 30 | TriggerKey triggerKey = new TriggerKey(jobTask.getName(), jobTask.getGroupName()); 31 | try { 32 | JobDetail jobDetail = JobBuilder.newJob((Class) Class.forName(jobTask.getClassName())) 33 | .withIdentity(jobKey) 34 | .build(); 35 | jobDetail.getJobDataMap().put("executeParams", jobTask.getExecuteParams()); 36 | CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()) 37 | .withMisfireHandlingInstructionDoNothing(); 38 | 39 | CronTrigger cronTrigger = TriggerBuilder.newTrigger() 40 | .withIdentity(triggerKey) 41 | .withSchedule(cronScheduleBuilder) 42 | .startNow() 43 | .build(); 44 | 45 | scheduler.scheduleJob(jobDetail, cronTrigger); 46 | } catch (ClassNotFoundException | SchedulerException e) { 47 | log.error("addJobTask failed", e); 48 | throw new BizException("addJobTask failed"); 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | @Override 55 | public boolean updateJobTask(JobTask jobTask) { 56 | boolean result = this.updateById(jobTask); 57 | if (result) { 58 | JobKey jobKey = new JobKey(jobTask.getName(), jobTask.getGroupName()); 59 | TriggerKey triggerKey = new TriggerKey(jobTask.getName(), jobTask.getGroupName()); 60 | try { 61 | CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); 62 | 63 | if (cronTrigger == null) { 64 | log.error("updateJobTask failed: trigger not found"); 65 | throw new BizException("updateJobTask failed: trigger not found"); 66 | } 67 | 68 | JobDetail jobDetail = scheduler.getJobDetail(jobKey); 69 | jobDetail.getJobDataMap().put("executeParams", jobTask.getExecuteParams()); 70 | 71 | CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()) 72 | .withMisfireHandlingInstructionDoNothing(); 73 | 74 | cronTrigger = cronTrigger.getTriggerBuilder() 75 | .withIdentity(triggerKey) 76 | .withSchedule(cronScheduleBuilder) 77 | .build(); 78 | 79 | scheduler.rescheduleJob(triggerKey, cronTrigger); 80 | } catch (SchedulerException e) { 81 | log.error("updateJobTask failed", e); 82 | throw new BizException("updateJobTask failed"); 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | @Override 89 | public boolean deleteJobTask(Integer id) { 90 | JobTask jobTask = this.getById(id); 91 | if (jobTask != null) { 92 | JobKey jobKey = new JobKey(jobTask.getName(), jobTask.getGroupName()); 93 | TriggerKey triggerKey = new TriggerKey(jobTask.getName(), jobTask.getGroupName()); 94 | try { 95 | scheduler.pauseTrigger(triggerKey); 96 | scheduler.unscheduleJob(triggerKey); 97 | scheduler.deleteJob(jobKey); 98 | } catch (SchedulerException e) { 99 | log.error("deleteJobTask failed", e); 100 | throw new BizException("deleteJobTask failed"); 101 | } 102 | } 103 | return this.removeById(id); 104 | } 105 | 106 | @Override 107 | public JobTask getJobTaskById(Integer id) { 108 | return this.getById(id); 109 | } 110 | 111 | @Override 112 | public List listAllJobTasks() { 113 | return this.list(); 114 | } 115 | 116 | @Override 117 | public boolean startJobTask(Integer id) { 118 | JobTask jobTask = this.getById(id); 119 | if (jobTask != null) { 120 | JobKey jobKey = new JobKey(jobTask.getName(), jobTask.getGroupName()); 121 | try { 122 | scheduler.resumeJob(jobKey); 123 | } catch (SchedulerException e) { 124 | log.error("startJobTask failed", e); 125 | throw new BizException("startJobTask failed"); 126 | } 127 | jobTask.setStatus(1); 128 | this.updateById(jobTask); 129 | return true; 130 | } 131 | return false; 132 | } 133 | 134 | @Override 135 | public boolean stopJobTask(Integer id) { 136 | JobTask jobTask = this.getById(id); 137 | if (jobTask != null) { 138 | JobKey jobKey = new JobKey(jobTask.getName(), jobTask.getGroupName()); 139 | try { 140 | scheduler.pauseJob(jobKey); 141 | } catch (SchedulerException e) { 142 | log.error("stopJobTask failed", e); 143 | throw new BizException("stopJobTask failed"); 144 | } 145 | jobTask.setStatus(0); 146 | this.updateById(jobTask); 147 | return true; 148 | } 149 | return false; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/utils/CloudflareUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | import org.springframework.http.*; 7 | import org.springframework.web.client.RestTemplate; 8 | import org.springframework.web.util.UriComponentsBuilder; 9 | import top.sssd.ddns.config.ApplicationContextProvider; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | /** 16 | * @author sssd 17 | * @created 2023-08-08-10:57 18 | */ 19 | 20 | public class CloudflareUtils { 21 | private CloudflareUtils() { 22 | } 23 | 24 | @Data 25 | public static class CloudflareQueryResponse { 26 | private List errors; 27 | private List messages; 28 | private Boolean success; 29 | private List result = new ArrayList<>(); 30 | } 31 | 32 | @Data 33 | public static class CloudflareResponse { 34 | private List errors; 35 | private List messages; 36 | private Boolean success; 37 | private SimpleContent result; 38 | } 39 | 40 | @Data 41 | @Accessors(chain = true) 42 | public static class SimpleContent { 43 | private String name; 44 | private Boolean proxied = true; 45 | private String content; 46 | private String type; 47 | private String id; 48 | private Boolean locked; 49 | private Map meta; 50 | private List tags; 51 | } 52 | 53 | public static RestTemplate getRestTemplate(){ 54 | return ApplicationContextProvider.getContext().getBean(RestTemplate.class); 55 | } 56 | 57 | private static final String BASE_URL = "https://api.cloudflare.com/client/v4/zones/"; 58 | 59 | private static final String AUTH_HEADER = "Authorization"; 60 | 61 | private static final String BEARER = "bearer"; 62 | 63 | private static final String ROUTE_PATH = "/dns_records"; 64 | 65 | 66 | /** 67 | * 根据子域名和解析类型查询域名列表 68 | * 69 | * @param zoneId 区域ID 70 | * @param accessKeySecret API_TOKEN 71 | * @param subDomain 子域名 72 | * @param recordType 解析类型 73 | * @return 74 | */ 75 | public static CloudflareQueryResponse getSubDomainParseList(String zoneId, String accessKeySecret, String subDomain, String recordType) { 76 | // 设置请求头,包括Authorization和Content-Type 77 | HttpHeaders headers = new HttpHeaders(); 78 | headers.set(AUTH_HEADER, BEARER + " " + accessKeySecret); 79 | headers.setContentType(MediaType.APPLICATION_JSON); 80 | // 构建URI参数 81 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BASE_URL) 82 | .path(zoneId + ROUTE_PATH) 83 | .queryParam("type", recordType) 84 | .queryParam("name", subDomain); 85 | // 构建RequestEntity 86 | RequestEntity requestEntity = new RequestEntity<>(headers, HttpMethod.GET, builder.build().toUri()); 87 | //发送请求 88 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CloudflareQueryResponse.class); 89 | return response.getBody(); 90 | } 91 | 92 | public static CloudflareQueryResponse getSubDomainParseList(String zoneId, String accessKeySecret, String subDomain, String recordType, String ip) { 93 | // 设置请求头,包括Authorization和Content-Type 94 | HttpHeaders headers = new HttpHeaders(); 95 | headers.set(AUTH_HEADER, BEARER + " " + accessKeySecret); 96 | headers.setContentType(MediaType.APPLICATION_JSON); 97 | // 构建URI参数 98 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BASE_URL) 99 | .path(zoneId + ROUTE_PATH) 100 | .queryParam("type", recordType) 101 | .queryParam("name", subDomain) 102 | .queryParam("content", ip); 103 | // 构建RequestEntity 104 | RequestEntity requestEntity = new RequestEntity<>(headers, HttpMethod.GET, builder.build().toUri()); 105 | //发送请求 106 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CloudflareQueryResponse.class); 107 | return response.getBody(); 108 | } 109 | 110 | 111 | /** 112 | * @param zoneId 区域ID 113 | * @param accessKeySecret API_TOKEN 114 | * @param domain 子域名 115 | * @param recordType 解析类型 116 | * @param ip 解析IP 117 | * @return 118 | * @throws JsonProcessingException 119 | */ 120 | public static CloudflareResponse add(String zoneId, String accessKeySecret, String domain, String recordType, String ip) throws JsonProcessingException { 121 | // 设置请求头,包括Authorization和Content-Type 122 | HttpHeaders headers = new HttpHeaders(); 123 | headers.set(AUTH_HEADER, BEARER + " " + accessKeySecret); 124 | headers.setContentType(MediaType.APPLICATION_JSON); 125 | // 构建URI参数 126 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BASE_URL).path(zoneId + ROUTE_PATH); 127 | 128 | SimpleContent simpleContent = new SimpleContent().setType(recordType).setName(domain).setContent(ip); 129 | // 构建RequestEntity 130 | RequestEntity requestEntity = new RequestEntity<>(simpleContent, headers, HttpMethod.POST, builder.build().toUri()); 131 | //发送请求 132 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CloudflareResponse.class); 133 | return response.getBody(); 134 | } 135 | 136 | /** 137 | * 获取记录ID 138 | * 139 | * @param zoneId 140 | * @param accessKeySecret 141 | * @param subDomain 142 | * @param recordType 143 | * @return 144 | */ 145 | public static String getId(String zoneId, String accessKeySecret, String subDomain, String recordType, String ip) { 146 | CloudflareQueryResponse response = getSubDomainParseList(zoneId, accessKeySecret, subDomain, recordType, ip); 147 | return response.getResult().get(0).getId(); 148 | } 149 | 150 | 151 | /** 152 | * @param zoneId 153 | * @param accessKeySecret 154 | * @param subDomain 155 | * @param recordType 156 | * @return 157 | */ 158 | public static String getIpBySubDomainWithType(String zoneId, String accessKeySecret, String subDomain, String recordType) { 159 | CloudflareQueryResponse cloudflareResponse = getSubDomainParseList(zoneId, accessKeySecret, subDomain, recordType); 160 | return cloudflareResponse.getResult().get(0).getContent(); 161 | } 162 | 163 | /** 164 | * 删除记录 165 | * 166 | * @param zoneId 167 | * @param accessKeySecret 168 | * @param domain 169 | * @param recordType 170 | * @return 171 | */ 172 | public static CloudflareResponse delete(String zoneId, String accessKeySecret, String domain, String recordType,String ip) { 173 | // 设置请求头,包括Authorization和Content-Type 174 | HttpHeaders headers = new HttpHeaders(); 175 | headers.set(AUTH_HEADER, BEARER + " " + accessKeySecret); 176 | headers.setContentType(MediaType.APPLICATION_JSON); 177 | // 构建URI参数 178 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BASE_URL) 179 | .path(zoneId + ROUTE_PATH) 180 | .path("/" + getId(zoneId, accessKeySecret, domain, recordType,ip)); 181 | 182 | // 构建RequestEntity 183 | RequestEntity requestEntity = new RequestEntity<>(headers, HttpMethod.DELETE, builder.build().toUri()); 184 | //发送请求 185 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CloudflareResponse.class); 186 | return response.getBody(); 187 | } 188 | 189 | 190 | /** 191 | * 更新记录 192 | * 193 | * @param zoneId 194 | * @param accessKeySecret 195 | * @param domain 196 | * @param recordType 197 | * @param ip 198 | * @return 199 | */ 200 | public static CloudflareResponse update(String zoneId, String accessKeySecret, String domain, String recordType, String ip,String recordId) { 201 | // 设置请求头,包括Authorization和Content-Type 202 | HttpHeaders headers = new HttpHeaders(); 203 | headers.set(AUTH_HEADER, BEARER + " " + accessKeySecret); 204 | headers.setContentType(MediaType.APPLICATION_JSON); 205 | // 构建URI参数 206 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BASE_URL) 207 | .path(zoneId + ROUTE_PATH) 208 | .path("/" + recordId); 209 | 210 | SimpleContent simpleContent = new SimpleContent().setType(recordType).setName(domain).setContent(ip); 211 | // 构建RequestEntity 212 | RequestEntity requestEntity = new RequestEntity<>(simpleContent, headers, HttpMethod.PUT, builder.build().toUri()); 213 | //发送请求 214 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CloudflareResponse.class); 215 | return response.getBody(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/handler/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.handler; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.ConversionNotSupportedException; 7 | import org.springframework.beans.TypeMismatchException; 8 | import org.springframework.http.converter.HttpMessageNotReadableException; 9 | import org.springframework.http.converter.HttpMessageNotWritableException; 10 | import org.springframework.validation.BindException; 11 | import org.springframework.validation.FieldError; 12 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 13 | import org.springframework.web.HttpMediaTypeNotSupportedException; 14 | import org.springframework.web.HttpRequestMethodNotSupportedException; 15 | import org.springframework.web.bind.MethodArgumentNotValidException; 16 | import org.springframework.web.bind.MissingPathVariableException; 17 | import org.springframework.web.bind.MissingServletRequestParameterException; 18 | import org.springframework.web.bind.ServletRequestBindingException; 19 | import org.springframework.web.bind.annotation.ExceptionHandler; 20 | import org.springframework.web.bind.annotation.RestControllerAdvice; 21 | import org.springframework.web.context.request.async.AsyncRequestTimeoutException; 22 | import org.springframework.web.multipart.support.MissingServletRequestPartException; 23 | import org.springframework.web.servlet.NoHandlerFoundException; 24 | import top.sssd.ddns.common.AmisResult; 25 | import top.sssd.ddns.common.BizException; 26 | 27 | import javax.annotation.Resource; 28 | import javax.validation.ConstraintViolationException; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.stream.Collectors; 33 | 34 | import static top.sssd.ddns.common.constant.ExceptionConstant.RECORD_EXISTS; 35 | 36 | /** 37 | * @author sssd 38 | */ 39 | @RestControllerAdvice 40 | @Slf4j 41 | public class GlobalExceptionHandler { 42 | 43 | @Resource 44 | private ObjectMapper objectMapper; 45 | 46 | @ExceptionHandler({Exception.class}) 47 | public AmisResult exceptionHandler(Exception exception) { 48 | log.error("Exception info:{}", exception.getMessage()); 49 | if (exception.getMessage().contains(RECORD_EXISTS)) { 50 | return AmisResult.fail("DNS记录已存在"); 51 | } 52 | return AmisResult.fail(exception.getMessage()); 53 | } 54 | 55 | @ExceptionHandler({ConstraintViolationException.class}) 56 | public AmisResult constraintViolationExceptionHandler(ConstraintViolationException violationException) { 57 | log.error("valid exception info:{}", violationException.getMessage()); 58 | return AmisResult.fail(violationException.getMessage()); 59 | } 60 | 61 | @ExceptionHandler({BindException.class}) 62 | public AmisResult> bindExceptionHandler(BindException e) throws JsonProcessingException { 63 | Map map = e.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); 64 | return AmisResult.fail(objectMapper.writeValueAsString(map)); 65 | } 66 | 67 | 68 | @ExceptionHandler(MethodArgumentNotValidException.class) 69 | public AmisResult> methodArgumentNotValidException(MethodArgumentNotValidException e) throws JsonProcessingException { 70 | List list = e.getBindingResult().getFieldErrors().stream().collect(Collectors.toList()); 71 | return AmisResult.fail(objectMapper.writeValueAsString(list)); 72 | } 73 | 74 | @ExceptionHandler(NoHandlerFoundException.class) 75 | public AmisResult noHandlerFoundException(NoHandlerFoundException e) throws JsonProcessingException { 76 | HashMap errorMap = new HashMap<>(); 77 | errorMap.put("传入的头信息:", e.getHeaders().toSingleValueMap()); 78 | errorMap.put("请求的方法:", e.getHttpMethod()); 79 | errorMap.put("请求的url:", e.getRequestURL()); 80 | log.error(objectMapper.writeValueAsString(errorMap)); 81 | return AmisResult.fail(e.getMessage()); 82 | } 83 | 84 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 85 | public AmisResult httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws JsonProcessingException { 86 | HashMap errorMap = new HashMap<>(); 87 | errorMap.put("请求的方法:", e.getMethod()); 88 | errorMap.put("支持请求的方法:", e.getSupportedMethods()); 89 | log.error(objectMapper.writeValueAsString(errorMap)); 90 | return AmisResult.fail(e.getMessage()); 91 | } 92 | 93 | @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 94 | public AmisResult httpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) throws JsonProcessingException { 95 | HashMap errorMap = new HashMap<>(); 96 | errorMap.put("请求的文件类型:", e.getContentType()); 97 | log.error(objectMapper.writeValueAsString(errorMap)); 98 | return AmisResult.fail(e.getMessage()); 99 | } 100 | 101 | @ExceptionHandler(MissingPathVariableException.class) 102 | public AmisResult missingPathVariableException(MissingPathVariableException e) throws JsonProcessingException { 103 | HashMap errorMap = new HashMap<>(); 104 | errorMap.put("路径参数的名称:", e.getVariableName()); 105 | errorMap.put("请求参数:", e.getParameter()); 106 | log.error(objectMapper.writeValueAsString(errorMap)); 107 | return AmisResult.fail(e.getMessage()); 108 | } 109 | 110 | @ExceptionHandler(MissingServletRequestParameterException.class) 111 | public AmisResult missingServletRequestParameterException(MissingServletRequestParameterException e) throws JsonProcessingException { 112 | HashMap errorMap = new HashMap<>(); 113 | errorMap.put("请求参数的名称:", e.getParameterName()); 114 | errorMap.put("请求参数的类型:", e.getParameterType()); 115 | log.error(objectMapper.writeValueAsString(errorMap)); 116 | return AmisResult.fail(e.getMessage()); 117 | } 118 | 119 | @ExceptionHandler(TypeMismatchException.class) 120 | public AmisResult typeMismatchException(TypeMismatchException e) throws JsonProcessingException { 121 | 122 | HashMap errorMap = new HashMap<>(); 123 | errorMap.put("要求的类型:", e.getRequiredType()); 124 | errorMap.put("传入的值:", e.getValue()); 125 | errorMap.put("属性名称:", e.getPropertyName()); 126 | 127 | log.error(objectMapper.writeValueAsString(errorMap)); 128 | return AmisResult.fail().code(Integer.parseInt(e.getErrorCode())).message(objectMapper.writeValueAsString(errorMap)); 129 | } 130 | 131 | @ExceptionHandler(HttpMessageNotReadableException.class) 132 | public AmisResult httpMessageNotReadableException(HttpMessageNotReadableException e) throws JsonProcessingException { 133 | 134 | HashMap errorMap = new HashMap<>(); 135 | errorMap.put("输入的消息流的堆栈信息:", e.getStackTrace()); 136 | 137 | log.error(objectMapper.writeValueAsString(errorMap)); 138 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 139 | } 140 | 141 | @ExceptionHandler(HttpMessageNotWritableException.class) 142 | public AmisResult httpMessageNotWritableException(HttpMessageNotWritableException e) throws JsonProcessingException { 143 | 144 | HashMap errorMap = new HashMap<>(); 145 | errorMap.put("输入的消息流的堆栈信息:", e.getStackTrace()); 146 | 147 | 148 | log.error(objectMapper.writeValueAsString(errorMap)); 149 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 150 | } 151 | 152 | @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) 153 | public AmisResult httpMediaTypeNotAcceptableException(HttpMediaTypeNotAcceptableException e) throws JsonProcessingException { 154 | 155 | HashMap errorMap = new HashMap<>(); 156 | errorMap.put("媒体类型不支持的堆栈信息:", e.getStackTrace()); 157 | 158 | log.error(objectMapper.writeValueAsString(errorMap)); 159 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 160 | } 161 | 162 | 163 | @ExceptionHandler(ServletRequestBindingException.class) 164 | public AmisResult servletRequestBindingException(ServletRequestBindingException e) throws JsonProcessingException { 165 | HashMap errorMap = new HashMap<>(); 166 | errorMap.put("请求绑定异常的堆栈信息:", e.getStackTrace()); 167 | log.error(objectMapper.writeValueAsString(errorMap)); 168 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 169 | } 170 | 171 | 172 | @ExceptionHandler(ConversionNotSupportedException.class) 173 | public AmisResult conversionNotSupportedException(ConversionNotSupportedException e) throws JsonProcessingException { 174 | 175 | HashMap errorMap = new HashMap<>(); 176 | errorMap.put("不支持转换异常的属性名称:", e.getPropertyName()); 177 | errorMap.put("不支持转换异常的所需的类型:", e.getRequiredType()); 178 | 179 | log.error(objectMapper.writeValueAsString(errorMap)); 180 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 181 | } 182 | 183 | 184 | @ExceptionHandler(MissingServletRequestPartException.class) 185 | public AmisResult missingServletRequestPartException(MissingServletRequestPartException e) throws JsonProcessingException { 186 | 187 | HashMap errorMap = new HashMap<>(); 188 | errorMap.put("请求附件的名称:", e.getRequestPartName()); 189 | 190 | log.error(objectMapper.writeValueAsString(errorMap)); 191 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 192 | } 193 | 194 | 195 | @ExceptionHandler(AsyncRequestTimeoutException.class) 196 | public AmisResult asyncRequestTimeoutException(AsyncRequestTimeoutException e) throws JsonProcessingException { 197 | 198 | HashMap errorMap = new HashMap<>(); 199 | errorMap.put("异步请求超时异常的堆栈信息:", e.getStackTrace()); 200 | 201 | log.error(objectMapper.writeValueAsString(errorMap)); 202 | return AmisResult.fail(objectMapper.writeValueAsString(errorMap)); 203 | } 204 | 205 | @ExceptionHandler(BizException.class) 206 | public AmisResult bizException(BizException e) throws JsonProcessingException { 207 | 208 | HashMap errorMap = new HashMap<>(); 209 | errorMap.put(e.getMessage() + ":", e.getStackTrace()); 210 | 211 | log.error(objectMapper.writeValueAsString(errorMap)); 212 | return AmisResult.fail(e.getMessage()); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/resources/sql/ddns4j_mysql.sql: -------------------------------------------------------------------------------- 1 | SET NAMES utf8mb4; 2 | 3 | CREATE DATABASE IF NOT EXISTS ddns4j CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 4 | USE ddns4j; 5 | 6 | CREATE TABLE `changed_log` ( 7 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 8 | `content` varchar(255) NOT NULL COMMENT '内容', 9 | `insert_date` datetime NOT NULL COMMENT '插入时间', 10 | PRIMARY KEY (`id`) 11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 12 | 13 | CREATE TABLE IF NOT EXISTS `job_task` ( 14 | `id` int(11) NOT NULL AUTO_INCREMENT, 15 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务名称', 16 | `group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务分组', 17 | `cron_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务表达式', 18 | `class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务类名', 19 | `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务描述', 20 | `status` int(1) NULL DEFAULT 0 COMMENT '任务状态:0-停止,1-运行', 21 | PRIMARY KEY (`id`) USING BTREE 22 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '任务调度表' ROW_FORMAT = Dynamic; 23 | 24 | CREATE TABLE IF NOT EXISTS `parsing_record` ( 25 | `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id', 26 | `service_provider` int(11) NOT NULL COMMENT '服务提供商1 阿里云 2 腾讯云 3 cloudflare', 27 | `service_provider_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 28 | `service_provider_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 29 | `record_type` int(11) NOT NULL COMMENT '解析类型:1 AAAA 2 A', 30 | `get_ip_mode` int(11) NOT NULL COMMENT '获取ip方式: 1 interface 2 network 3 cmd', 31 | `get_ip_mode_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '当为interface时 https://myip4.ipip.net, https://ddns.oray.com/checkip, https://ip.3322.net, https://4.ipw.cn\r\n当为network时 是网卡信息\r\n当为cmd时 是bash或shell命令\r\n', 32 | `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '真实的公网ip', 33 | `domain` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '域名', 34 | `update_frequency` int(11) NOT NULL COMMENT '单位:分钟 1分钟 2分钟 5分钟 10分钟', 35 | `create_date` datetime NULL DEFAULT NULL COMMENT '创建时间', 36 | `update_date` datetime NULL DEFAULT NULL COMMENT '更新时间', 37 | `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者', 38 | `updater` bigint(20) NULL DEFAULT NULL COMMENT '更新者', 39 | PRIMARY KEY (`id`) USING BTREE 40 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '解析记录表' ROW_FORMAT = Dynamic; 41 | 42 | -- quartz自带表结构 43 | CREATE TABLE QRTZ_JOB_DETAILS( 44 | SCHED_NAME VARCHAR(120) NOT NULL, 45 | JOB_NAME VARCHAR(200) NOT NULL, 46 | JOB_GROUP VARCHAR(200) NOT NULL, 47 | DESCRIPTION VARCHAR(250) NULL, 48 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 49 | IS_DURABLE VARCHAR(1) NOT NULL, 50 | IS_NONCONCURRENT VARCHAR(1) NOT NULL, 51 | IS_UPDATE_DATA VARCHAR(1) NOT NULL, 52 | REQUESTS_RECOVERY VARCHAR(1) NOT NULL, 53 | JOB_DATA BLOB NULL, 54 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)) 55 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 56 | 57 | CREATE TABLE QRTZ_TRIGGERS ( 58 | SCHED_NAME VARCHAR(120) NOT NULL, 59 | TRIGGER_NAME VARCHAR(200) NOT NULL, 60 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 61 | JOB_NAME VARCHAR(200) NOT NULL, 62 | JOB_GROUP VARCHAR(200) NOT NULL, 63 | DESCRIPTION VARCHAR(250) NULL, 64 | NEXT_FIRE_TIME BIGINT(13) NULL, 65 | PREV_FIRE_TIME BIGINT(13) NULL, 66 | PRIORITY INTEGER NULL, 67 | TRIGGER_STATE VARCHAR(16) NOT NULL, 68 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 69 | START_TIME BIGINT(13) NOT NULL, 70 | END_TIME BIGINT(13) NULL, 71 | CALENDAR_NAME VARCHAR(200) NULL, 72 | MISFIRE_INSTR SMALLINT(2) NULL, 73 | JOB_DATA BLOB NULL, 74 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 75 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 76 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)) 77 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 78 | 79 | CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( 80 | SCHED_NAME VARCHAR(120) NOT NULL, 81 | TRIGGER_NAME VARCHAR(200) NOT NULL, 82 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 83 | REPEAT_COUNT BIGINT(7) NOT NULL, 84 | REPEAT_INTERVAL BIGINT(12) NOT NULL, 85 | TIMES_TRIGGERED BIGINT(10) NOT NULL, 86 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 87 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 88 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 89 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 90 | 91 | CREATE TABLE QRTZ_CRON_TRIGGERS ( 92 | SCHED_NAME VARCHAR(120) NOT NULL, 93 | TRIGGER_NAME VARCHAR(200) NOT NULL, 94 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 95 | CRON_EXPRESSION VARCHAR(120) NOT NULL, 96 | TIME_ZONE_ID VARCHAR(80), 97 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 98 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 99 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 100 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 101 | 102 | CREATE TABLE QRTZ_SIMPROP_TRIGGERS 103 | ( 104 | SCHED_NAME VARCHAR(120) NOT NULL, 105 | TRIGGER_NAME VARCHAR(200) NOT NULL, 106 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 107 | STR_PROP_1 VARCHAR(512) NULL, 108 | STR_PROP_2 VARCHAR(512) NULL, 109 | STR_PROP_3 VARCHAR(512) NULL, 110 | INT_PROP_1 INT NULL, 111 | INT_PROP_2 INT NULL, 112 | LONG_PROP_1 BIGINT NULL, 113 | LONG_PROP_2 BIGINT NULL, 114 | DEC_PROP_1 NUMERIC(13,4) NULL, 115 | DEC_PROP_2 NUMERIC(13,4) NULL, 116 | BOOL_PROP_1 VARCHAR(1) NULL, 117 | BOOL_PROP_2 VARCHAR(1) NULL, 118 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 119 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 120 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 121 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 122 | 123 | CREATE TABLE QRTZ_BLOB_TRIGGERS ( 124 | SCHED_NAME VARCHAR(120) NOT NULL, 125 | TRIGGER_NAME VARCHAR(200) NOT NULL, 126 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 127 | BLOB_DATA BLOB NULL, 128 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 129 | INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP), 130 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 131 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 132 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 133 | 134 | CREATE TABLE QRTZ_CALENDARS ( 135 | SCHED_NAME VARCHAR(120) NOT NULL, 136 | CALENDAR_NAME VARCHAR(200) NOT NULL, 137 | CALENDAR BLOB NOT NULL, 138 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)) 139 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 140 | 141 | CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( 142 | SCHED_NAME VARCHAR(120) NOT NULL, 143 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 144 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)) 145 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 146 | 147 | CREATE TABLE QRTZ_FIRED_TRIGGERS ( 148 | SCHED_NAME VARCHAR(120) NOT NULL, 149 | ENTRY_ID VARCHAR(95) NOT NULL, 150 | TRIGGER_NAME VARCHAR(200) NOT NULL, 151 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 152 | INSTANCE_NAME VARCHAR(200) NOT NULL, 153 | FIRED_TIME BIGINT(13) NOT NULL, 154 | SCHED_TIME BIGINT(13) NOT NULL, 155 | PRIORITY INTEGER NOT NULL, 156 | STATE VARCHAR(16) NOT NULL, 157 | JOB_NAME VARCHAR(200) NULL, 158 | JOB_GROUP VARCHAR(200) NULL, 159 | IS_NONCONCURRENT VARCHAR(1) NULL, 160 | REQUESTS_RECOVERY VARCHAR(1) NULL, 161 | PRIMARY KEY (SCHED_NAME,ENTRY_ID)) 162 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 163 | 164 | CREATE TABLE QRTZ_SCHEDULER_STATE ( 165 | SCHED_NAME VARCHAR(120) NOT NULL, 166 | INSTANCE_NAME VARCHAR(200) NOT NULL, 167 | LAST_CHECKIN_TIME BIGINT(13) NOT NULL, 168 | CHECKIN_INTERVAL BIGINT(13) NOT NULL, 169 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)) 170 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 171 | 172 | CREATE TABLE QRTZ_LOCKS ( 173 | SCHED_NAME VARCHAR(120) NOT NULL, 174 | LOCK_NAME VARCHAR(40) NOT NULL, 175 | PRIMARY KEY (SCHED_NAME,LOCK_NAME)) 176 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 177 | 178 | CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); 179 | CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); 180 | 181 | CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 182 | CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); 183 | CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); 184 | CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 185 | CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); 186 | CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); 187 | CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); 188 | CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); 189 | CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); 190 | CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); 191 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); 192 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); 193 | 194 | CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); 195 | CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); 196 | CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 197 | CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); 198 | CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); 199 | CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/service/impl/ParsingRecordServiceImpl.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; 4 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 5 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.util.StringUtils; 9 | import org.springframework.web.client.RestTemplate; 10 | import top.sssd.ddns.common.BizException; 11 | import top.sssd.ddns.common.enums.RecordTypeEnum; 12 | import top.sssd.ddns.common.enums.ServiceProviderEnum; 13 | import top.sssd.ddns.common.enums.UpdateFrequencyEnum; 14 | import top.sssd.ddns.common.utils.AmisPageUtils; 15 | import top.sssd.ddns.mapper.ParsingRecordMapper; 16 | import top.sssd.ddns.model.entity.JobTask; 17 | import top.sssd.ddns.model.entity.ParsingRecord; 18 | import top.sssd.ddns.model.response.NetWorkSelectResponse; 19 | import top.sssd.ddns.strategy.DynamicDnsStrategy; 20 | import top.sssd.ddns.service.IJobTaskService; 21 | import top.sssd.ddns.service.IParsingRecordService; 22 | import top.sssd.ddns.service.NetWorkService; 23 | import top.sssd.ddns.strategy.DynamicDnsServiceFactory; 24 | import top.sssd.ddns.task.DynamicDnsJob; 25 | 26 | import javax.annotation.Resource; 27 | import java.net.SocketException; 28 | import java.util.Arrays; 29 | import java.util.Collections; 30 | import java.util.List; 31 | import java.util.Objects; 32 | import java.util.stream.Collectors; 33 | 34 | import static top.sssd.ddns.common.constant.DDNSConstant.*; 35 | 36 | /** 37 | *

38 | * 解析记录表 服务实现类 39 | *

40 | * 41 | * @author sssd 42 | * @since 2023-03-19 43 | */ 44 | @Service 45 | @Slf4j 46 | public class ParsingRecordServiceImpl extends ServiceImpl implements IParsingRecordService { 47 | 48 | @Resource 49 | private IJobTaskService jobTaskService; 50 | 51 | @Resource 52 | private DefaultIdentifierGenerator defaultIdentifierGenerator; 53 | 54 | @Resource 55 | private DynamicDnsServiceFactory dnsServiceFactory; 56 | 57 | @Override 58 | public void add(ParsingRecord parsingRecord) throws Exception { 59 | DynamicDnsStrategy dynamicDnsService = dnsServiceFactory.getServiceInstance(parsingRecord.getServiceProvider()); 60 | 61 | String ip = getIp(parsingRecord); 62 | 63 | //后端唯一性校验 64 | ParsingRecord checkParsingRecord = this.lambdaQuery() 65 | .eq(ParsingRecord::getServiceProvider, parsingRecord.getServiceProvider()) 66 | .eq(ParsingRecord::getRecordType, parsingRecord.getRecordType()) 67 | .eq(ParsingRecord::getDomain, parsingRecord.getDomain()) 68 | .eq(ParsingRecord::getIp, parsingRecord.getIp()) 69 | .last("limit 1").one(); 70 | if (Objects.nonNull(checkParsingRecord)) { 71 | throw new BizException("同一域名,同一解析类型,同一ip,不能重复添加"); 72 | } 73 | if (dynamicDnsService.exist(parsingRecord.getServiceProviderId(), 74 | parsingRecord.getServiceProviderSecret(), 75 | parsingRecord.getDomain(), 76 | RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()))) { 77 | throw new BizException("该记录已在域名服务商中存在"); 78 | } 79 | this.save(parsingRecord); 80 | dynamicDnsService.add(parsingRecord, ip); 81 | //添加并启动一个定时任务 82 | addWithStartTask(parsingRecord); 83 | } 84 | 85 | @Override 86 | public void copy(ParsingRecord parsingRecord) throws Exception { 87 | long newId = defaultIdentifierGenerator.nextId(parsingRecord).longValue(); 88 | parsingRecord.setId(newId); 89 | add(parsingRecord); 90 | } 91 | 92 | @Override 93 | public void modify(ParsingRecord parsingRecord) throws Exception { 94 | DynamicDnsStrategy dynamicDnsService = dnsServiceFactory.getServiceInstance(parsingRecord.getServiceProvider()); 95 | 96 | ParsingRecord dbParsingRecord = this.getById(parsingRecord.getId()); 97 | if (Objects.isNull(dbParsingRecord)) { 98 | throw new BizException("该记录不存在"); 99 | } 100 | //后端唯一性校验 101 | ParsingRecord checkParsingRecord = this.lambdaQuery() 102 | .eq(ParsingRecord::getServiceProvider, parsingRecord.getServiceProvider()) 103 | .eq(ParsingRecord::getRecordType, parsingRecord.getRecordType()) 104 | .eq(ParsingRecord::getIp, parsingRecord.getIp()) 105 | .eq(ParsingRecord::getDomain, parsingRecord.getDomain()) 106 | .ne(ParsingRecord::getId, parsingRecord.getId()) 107 | .last("limit 1").one(); 108 | if (Objects.nonNull(checkParsingRecord)) { 109 | throw new BizException("同一域名,同一解析类型,同一ip,不允许重复更新"); 110 | } 111 | 112 | String dnsIp = null; 113 | dnsIp = dynamicDnsService.getIpBySubDomainWithType(dbParsingRecord); 114 | String recordId = dynamicDnsService.getRecordId(dbParsingRecord, dnsIp); 115 | String ip = getIp(parsingRecord); 116 | //是否修改了服务商相关信息 117 | if(updatedServiceProvider(dbParsingRecord,parsingRecord)||!ip.equals(dnsIp)){ 118 | dynamicDnsService.update(parsingRecord, ip, recordId); 119 | } 120 | this.updateById(parsingRecord); 121 | // 删除之前的定时任务 122 | JobTask one = jobTaskService.lambdaQuery().eq(JobTask::getName, dbParsingRecord.getId().toString()).one(); 123 | if (Objects.nonNull(one)) { 124 | jobTaskService.deleteJobTask(one.getId()); 125 | } 126 | // 添加并启动一个定时任务 127 | addWithStartTask(parsingRecord); 128 | } 129 | 130 | //是否修改了服务商相关信息 131 | public boolean updatedServiceProvider(ParsingRecord dbParsingRecord, ParsingRecord parsingRecord) { 132 | return !dbParsingRecord.getServiceProvider().equals(parsingRecord.getServiceProvider()) || 133 | !dbParsingRecord.getServiceProviderId().equals(parsingRecord.getServiceProviderId()) || 134 | !dbParsingRecord.getServiceProviderSecret().equals(parsingRecord.getServiceProviderSecret()) || 135 | !dbParsingRecord.getRecordType().equals(parsingRecord.getRecordType()) || 136 | !dbParsingRecord.getDomain().equals(parsingRecord.getDomain()); 137 | } 138 | 139 | private void addWithStartTask(ParsingRecord parsingRecord) { 140 | JobTask jobTask = new JobTask(); 141 | jobTask.setName(parsingRecord.getId().toString()); 142 | jobTask.setStatus(1); 143 | jobTask.setClassName(DynamicDnsJob.class.getName()); 144 | jobTask.setCronExpression(UpdateFrequencyEnum.getCronExpressionByCode(parsingRecord.getUpdateFrequency())); 145 | jobTask.setExecuteParams(parsingRecord); 146 | jobTaskService.addJobTask(jobTask); 147 | } 148 | 149 | @Override 150 | public void delete(Long id) throws Exception { 151 | ParsingRecord parsingRecord = this.getById(id); 152 | if (Objects.isNull(parsingRecord)) { 153 | throw new BizException("该记录不存在"); 154 | } 155 | DynamicDnsStrategy dynamicDnsService = dnsServiceFactory.getServiceInstance(parsingRecord.getServiceProvider()); 156 | 157 | if (!dynamicDnsService.exist(parsingRecord.getServiceProviderId(), 158 | parsingRecord.getServiceProviderSecret(), 159 | parsingRecord.getDomain(), 160 | RecordTypeEnum.getNameByIndex(parsingRecord.getRecordType()))) { 161 | throw new BizException("该记录在域名服务商中不存在"); 162 | } 163 | String ip = getIp(parsingRecord); 164 | dynamicDnsService.remove(parsingRecord, ip); 165 | this.removeById(id); 166 | // 2023/5/2 删除定时任务 167 | JobTask one = jobTaskService.lambdaQuery().eq(JobTask::getName, parsingRecord.getId().toString()).one(); 168 | if (Objects.nonNull(one)) { 169 | jobTaskService.deleteJobTask(one.getId()); 170 | } 171 | } 172 | 173 | @Resource 174 | private RestTemplate restTemplate; 175 | 176 | @Override 177 | public String getIp(ParsingRecord parsingRecord) { 178 | Integer recordType = parsingRecord.getRecordType(); 179 | Integer getIpMode = parsingRecord.getGetIpMode(); 180 | String getIpModeValue = parsingRecord.getGetIpModeValue().trim(); 181 | 182 | if (recordType.equals(RECORD_TYPE_AAAA)) { 183 | // AAAA record type (ipv6) 184 | return handleIpv6(getIpMode, getIpModeValue, parsingRecord); 185 | } else if (recordType.equals(RECORD_TYPE_A)) { 186 | // A record type (ipv4) 187 | return handleIpv4(getIpMode, getIpModeValue, parsingRecord); 188 | }else{ 189 | throw new BizException("参数错误-recordType:"+recordType); 190 | } 191 | } 192 | 193 | private String handleIpv6(Integer getIpMode, String getIpModeValue, ParsingRecord parsingRecord) { 194 | if (IP_MODE_INTERFACE.equals(getIpMode)) { 195 | String ipv6 = restTemplate.getForObject(getIpModeValue, String.class); 196 | if (!StringUtils.hasText(ipv6)) { 197 | throw new BizException("通过网络接口获取ipv6地址失败,请检查网卡是否分配ipv6地址"); 198 | } 199 | parsingRecord.setIp(ipv6); 200 | return ipv6.trim(); 201 | } else if (IP_MODE_NETWORK.equals(getIpMode)) { 202 | if (!StringUtils.hasText(getIpModeValue)) { 203 | throw new BizException("通过本地网卡获取ipv6地址失败,请检查网卡是否分配ipv6地址"); 204 | } 205 | parsingRecord.setIp(getIpModeValue); 206 | return getIpModeValue; 207 | }else{ 208 | throw new BizException("参数错误-getIpMode:"+getIpMode); 209 | } 210 | } 211 | 212 | private String handleIpv4(Integer getIpMode, String getIpModeValue, ParsingRecord parsingRecord) { 213 | if (IP_MODE_INTERFACE.equals(getIpMode)) { 214 | String ipv4 = restTemplate.getForObject(getIpModeValue, String.class); 215 | if (!StringUtils.hasText(ipv4)) { 216 | throw new BizException("通过网络接口获取ipv4地址失败,请检查网卡是否分配ipv4地址"); 217 | } 218 | parsingRecord.setIp(ipv4); 219 | return ipv4.trim(); 220 | } else if (IP_MODE_NETWORK.equals(getIpMode)) { 221 | parsingRecord.setIp(getIpModeValue); 222 | return getIpModeValue; 223 | }else{ 224 | throw new BizException("参数错误-getIpMode:"+getIpMode); 225 | } 226 | } 227 | 228 | @Resource 229 | private NetWorkService netWorkService; 230 | 231 | @Override 232 | public List getModeIpValue(Integer getIpMode, Integer recordType) throws SocketException { 233 | if (IP_MODE_INTERFACE.equals(getIpMode)) { 234 | if (RECORD_TYPE_AAAA.equals(recordType)) { 235 | return Arrays.stream(IPV6_INTERFACE_VALUES).map(item -> { 236 | NetWorkSelectResponse netWorkSelectResponse = new NetWorkSelectResponse(); 237 | netWorkSelectResponse.setLabel(item); 238 | netWorkSelectResponse.setValue(item); 239 | return netWorkSelectResponse; 240 | }).collect(Collectors.toList()); 241 | } else if (RECORD_TYPE_A.equals(recordType)) { 242 | return Arrays.stream(IPV4_INTERFACE_VALUES).map(item -> { 243 | NetWorkSelectResponse netWorkSelectResponse = new NetWorkSelectResponse(); 244 | netWorkSelectResponse.setLabel(item); 245 | netWorkSelectResponse.setValue(item); 246 | return netWorkSelectResponse; 247 | }).collect(Collectors.toList()); 248 | } 249 | } else if (IP_MODE_NETWORK.equals(getIpMode)) { 250 | return netWorkService.networks(recordType); 251 | } 252 | return Collections.emptyList(); 253 | } 254 | 255 | @Override 256 | public AmisPageUtils queryPage(ParsingRecord parsingRecord) { 257 | Page pageList = lambdaQuery() 258 | .eq(Objects.nonNull(parsingRecord.getServiceProvider()), ParsingRecord::getServiceProvider, parsingRecord.getServiceProvider()) 259 | .eq(StringUtils.hasText(parsingRecord.getDomain()), ParsingRecord::getDomain, parsingRecord.getDomain()) 260 | .eq(Objects.nonNull(parsingRecord.getRecordType()), ParsingRecord::getRecordType, parsingRecord.getRecordType()) 261 | .ge(Objects.nonNull(parsingRecord.getCreateDate()), ParsingRecord::getCreateDate, parsingRecord.getCreateDate()) 262 | .le(Objects.nonNull(parsingRecord.getUpdateDate()), ParsingRecord::getUpdateDate, parsingRecord.getUpdateDate()) 263 | .page(new Page(parsingRecord.getPage(), parsingRecord.getPerPage())); 264 | 265 | List resultList = pageList.getRecords().stream().map(item -> { 266 | String serviceProviderName = ServiceProviderEnum.getNameByIndex(item.getServiceProvider()); 267 | String recordTypeName = RecordTypeEnum.getNameByIndex(item.getRecordType()); 268 | item.setServiceProviderName(serviceProviderName); 269 | item.setRecordTypeName(recordTypeName); 270 | return item; 271 | }).collect(Collectors.toList()); 272 | pageList.setRecords(resultList); 273 | 274 | return new AmisPageUtils<>(pageList); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/main/java/top/sssd/ddns/utils/TencentDnsUtils.java: -------------------------------------------------------------------------------- 1 | package top.sssd.ddns.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import lombok.Data; 6 | import lombok.experimental.Accessors; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.http.HttpMethod; 10 | import org.springframework.http.RequestEntity; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.CollectionUtils; 14 | import org.springframework.web.client.RestTemplate; 15 | import org.springframework.web.util.UriComponentsBuilder; 16 | import top.sssd.ddns.common.BizException; 17 | import top.sssd.ddns.config.ApplicationContextProvider; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Objects; 22 | import java.util.TreeMap; 23 | 24 | import static top.sssd.ddns.utils.TencentCloudAPITC3Singer.HOST; 25 | 26 | 27 | /** 28 | * @author sssd 29 | * @created 2023-05-06-16:13 30 | */ 31 | @Slf4j 32 | @Component 33 | public class TencentDnsUtils { 34 | 35 | private TencentDnsUtils(){} 36 | 37 | private static final String CREATE_RECORD_ACTION = "CreateRecord"; 38 | private static final String MODIFY_RECORD_ACTION = "ModifyRecord"; 39 | private static final String DELETE_RECORD_ACTION = "DeleteRecord"; 40 | private static final String DESCRIBE_RECORD_LIST_ACTION = "DescribeRecordList"; 41 | private static final String PROTOCOL = "https://"; 42 | 43 | public static ObjectMapper getObjectMapper(){ 44 | return ApplicationContextProvider.getContext().getBean(ObjectMapper.class); 45 | } 46 | 47 | public static RestTemplate getRestTemplate(){ 48 | return ApplicationContextProvider.getContext().getBean(RestTemplate.class); 49 | } 50 | 51 | /** 52 | * 创建记录实体 53 | * @ https://cloud.tencent.com/document/product/1427/56180 54 | */ 55 | @Data 56 | @Accessors(chain = true) 57 | public static class CreateRecordRequest { 58 | @JsonProperty("Domain") 59 | private String domain; 60 | @JsonProperty("SubDomain") 61 | private String subDomain; 62 | @JsonProperty("RecordType") 63 | private String recordType; 64 | @JsonProperty("Value") 65 | private String value; 66 | @JsonProperty("RecordLine") 67 | private String recordLine = "默认"; 68 | } 69 | 70 | @Data 71 | public static class CreateRecordResponse { 72 | @JsonProperty("Response") 73 | private CreateResponse response; 74 | } 75 | 76 | @Data 77 | public static class CreateResponse{ 78 | @JsonProperty("Error") 79 | private Error error; 80 | @JsonProperty("RecordId") 81 | private String recordId; 82 | @JsonProperty("RequestId") 83 | private String requestId; 84 | } 85 | 86 | /** 87 | * 删除记录实体 88 | * @ https://cloud.tencent.com/document/product/1427/56176 89 | */ 90 | @Data 91 | @Accessors(chain = true) 92 | public static class DeleteRecordRequest { 93 | @JsonProperty("Domain") 94 | private String domain; 95 | @JsonProperty("RecordId") 96 | private Integer recordId; 97 | } 98 | 99 | @Data 100 | public static class DeleteRecordResponse { 101 | @JsonProperty("Response") 102 | private DeleteResponse response; 103 | } 104 | 105 | @Data 106 | public static class DeleteResponse{ 107 | @JsonProperty("RequestId") 108 | private String requestId; 109 | @JsonProperty("Error") 110 | private Error error; 111 | } 112 | 113 | /** 114 | * 获取列表实体 115 | * @ https://cloud.tencent.com/document/product/1427/56166 116 | */ 117 | @Data 118 | @Accessors(chain = true) 119 | public static class ListRecordRequest { 120 | @JsonProperty("Domain") 121 | private String domain; 122 | @JsonProperty("Subdomain") 123 | private String subDomain; 124 | @JsonProperty("RecordType") 125 | private String recordType; 126 | @JsonProperty("RecordLine") 127 | private String recordLine = "默认"; 128 | } 129 | 130 | @Data 131 | public static class ListRecordResponse { 132 | @JsonProperty("Response") 133 | private ListResponse response; 134 | } 135 | 136 | @Data 137 | public static class ListResponse { 138 | @JsonProperty("Error") 139 | private Error error; 140 | @JsonProperty("RequestId") 141 | private String requestId; 142 | @JsonProperty("RecordList") 143 | private List recordList = new ArrayList<>(); 144 | } 145 | 146 | @Data 147 | public static class RecordListItem { 148 | @JsonProperty("RecordId") 149 | private String recordId; 150 | @JsonProperty("Value") 151 | private String value; 152 | } 153 | 154 | /** 155 | * 更新记录实体对象 156 | * @ https://cloud.tencent.com/document/product/1427/56157 157 | */ 158 | @Data 159 | @Accessors(chain = true) 160 | public static class UpdateRecordRequest { 161 | @JsonProperty("Domain") 162 | private String domain; 163 | @JsonProperty("SubDomain") 164 | private String subDomain; 165 | @JsonProperty("RecordType") 166 | private String recordType; 167 | @JsonProperty("RecordId") 168 | private Integer recordId; 169 | @JsonProperty("Value") 170 | private String value; 171 | @JsonProperty("RecordLine") 172 | private String recordLine = "默认"; 173 | } 174 | 175 | @Data 176 | public static class UpdateRecordResponse { 177 | @JsonProperty("Response") 178 | private UpdateResponse response; 179 | } 180 | 181 | @Data 182 | public static class UpdateResponse{ 183 | @JsonProperty("RecordId") 184 | private Integer recordId; 185 | @JsonProperty("RequestId") 186 | private String requestId; 187 | @JsonProperty("Error") 188 | private Error error; 189 | } 190 | 191 | @Data 192 | public static class Error{ 193 | @JsonProperty("Code") 194 | private String code; 195 | 196 | @JsonProperty("Message") 197 | private String message; 198 | } 199 | 200 | 201 | /** 202 | * 获取解析记录ID 203 | * 204 | * @param domain 205 | * @param subDomain 206 | * @param recordType 207 | * @param secretId 208 | * @param secretKey 209 | * @return 210 | */ 211 | public static String getRecordId(String domain, String subDomain, String recordType, 212 | String secretId, String secretKey) throws Exception { 213 | ListRecordResponse listRecordResponse = getRecordList(domain, subDomain, recordType, secretId, secretKey); 214 | 215 | if(Objects.isNull(listRecordResponse)){ 216 | throw new BizException("TencentCloud 查询列表记录失败 没有来自腾讯云的响应"); 217 | } 218 | TencentDnsUtils.ListResponse listResponse = listRecordResponse.getResponse(); 219 | if(Objects.isNull(listResponse)){ 220 | throw new BizException("TencentCloud 查询列表记录失败 腾讯云的列表记录响应对象为空"); 221 | } 222 | TencentDnsUtils.Error error = listResponse.getError(); 223 | if(Objects.nonNull(error)){ 224 | throw new BizException("TencentCloud 查询列表记录失败"+error.getMessage()); 225 | } 226 | List recordList = listResponse.getRecordList(); 227 | if(CollectionUtils.isEmpty(recordList)){ 228 | throw new BizException("TencentCloud 查询列表为空 "); 229 | } 230 | 231 | return recordList.get(0).getRecordId(); 232 | } 233 | 234 | /** 235 | * 根据域名和解析记录获取ip 236 | * 237 | * @param domain 238 | * @param subDomain 239 | * @param recordType 240 | * @param secretId 241 | * @param secretKey 242 | * @return 243 | */ 244 | public static String getIpBySubDomainWithType(String domain, String subDomain, String recordType, 245 | String secretId, String secretKey) throws Exception { 246 | ListRecordResponse listRecordResponse = getRecordList(domain, subDomain, recordType, secretId, secretKey); 247 | if(Objects.isNull(listRecordResponse)){ 248 | throw new BizException("TencentCloud 查询列表记录失败 没有来自腾讯云的响应"); 249 | } 250 | TencentDnsUtils.ListResponse listResponse = listRecordResponse.getResponse(); 251 | if(Objects.isNull(listResponse)){ 252 | throw new BizException("TencentCloud 查询列表记录失败 腾讯云的列表记录响应对象为空"); 253 | } 254 | TencentDnsUtils.Error error = listResponse.getError(); 255 | if(Objects.nonNull(error)){ 256 | throw new BizException("TencentCloud 查询列表记录失败"+error.getMessage()); 257 | } 258 | List recordList = listResponse.getRecordList(); 259 | if(recordList.isEmpty()){ 260 | throw new BizException("TencentCloud 查询列表为空 "); 261 | } 262 | return recordList.get(0).getValue(); 263 | } 264 | 265 | public static ListRecordResponse getRecordList(String domain, String subDomain, String recordType, 266 | String secretId, String secretKey) throws Exception { 267 | 268 | 269 | ListRecordRequest listRecordRequest = new ListRecordRequest().setDomain(domain).setSubDomain(subDomain).setRecordType(recordType); 270 | String jsonBody = getObjectMapper().writeValueAsString(listRecordRequest); 271 | 272 | TreeMap headerMap = TencentCloudAPITC3Singer.buildSignRequestHeaderWithBody(secretId, secretKey, DESCRIBE_RECORD_LIST_ACTION, jsonBody); 273 | HttpHeaders headers = new HttpHeaders(); 274 | headers.setAll(headerMap); 275 | 276 | // 构建URI参数 277 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(PROTOCOL + HOST); 278 | // 构建RequestEntity 279 | RequestEntity requestEntity = new RequestEntity<>(jsonBody, headers, HttpMethod.POST, builder.build().toUri()); 280 | //发送请求 281 | ResponseEntity response = getRestTemplate().exchange(requestEntity, ListRecordResponse.class); 282 | return response.getBody(); 283 | } 284 | 285 | /** 286 | * 添加一条解析记录 287 | * 288 | * @param domain 289 | * @param subDomain 290 | * @param recordType 291 | * @param ip 292 | * @return 293 | * @throws Exception 294 | */ 295 | public static CreateRecordResponse createRecord(String domain, String subDomain, String recordType, 296 | String secretId, String secretKey, String ip) throws Exception { 297 | 298 | 299 | CreateRecordRequest createRecordRequest = new CreateRecordRequest().setDomain(domain).setSubDomain(subDomain).setRecordType(recordType).setValue(ip); 300 | String jsonBody = getObjectMapper().writeValueAsString(createRecordRequest); 301 | 302 | TreeMap headerMap = TencentCloudAPITC3Singer.buildSignRequestHeaderWithBody(secretId, secretKey, CREATE_RECORD_ACTION, jsonBody); 303 | HttpHeaders headers = new HttpHeaders(); 304 | headers.setAll(headerMap); 305 | // 构建URI参数 306 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(PROTOCOL + HOST); 307 | // 构建RequestEntity 308 | RequestEntity requestEntity = new RequestEntity<>(jsonBody, headers, HttpMethod.POST, builder.build().toUri()); 309 | //发送请求 310 | ResponseEntity response = getRestTemplate().exchange(requestEntity, CreateRecordResponse.class); 311 | return response.getBody(); 312 | } 313 | 314 | /** 315 | * 更新记录 316 | * 317 | * @param domain 318 | * @param subDomain 319 | * @param recordType 320 | * @param secretId 321 | * @param secretKey 322 | * @param ip 323 | * @param recordId 324 | * @return 325 | */ 326 | public static UpdateRecordResponse updateRecord(String domain, String subDomain, String recordType, 327 | String secretId, String secretKey, String ip, Integer recordId) throws Exception { 328 | 329 | 330 | UpdateRecordRequest updateRecordRequest = new UpdateRecordRequest().setDomain(domain).setValue(ip).setSubDomain(subDomain).setRecordType(recordType).setRecordId(recordId); 331 | String jsonBody = getObjectMapper().writeValueAsString(updateRecordRequest); 332 | 333 | TreeMap headerMap = TencentCloudAPITC3Singer.buildSignRequestHeaderWithBody(secretId, secretKey, MODIFY_RECORD_ACTION, jsonBody); 334 | HttpHeaders headers = new HttpHeaders(); 335 | headers.setAll(headerMap); 336 | // 构建URI参数 337 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(PROTOCOL + HOST); 338 | // 构建RequestEntity 339 | RequestEntity requestEntity = new RequestEntity<>(jsonBody, headers, HttpMethod.POST, builder.build().toUri()); 340 | //发送请求 341 | ResponseEntity response = getRestTemplate().exchange(requestEntity, UpdateRecordResponse.class); 342 | return response.getBody(); 343 | } 344 | 345 | /** 346 | * 删除解析记录 347 | * 348 | * @param domain 349 | * @param secretId 350 | * @param secretKey 351 | * @param recordId 352 | */ 353 | public static DeleteRecordResponse deleteRecord(String domain, String secretId, String secretKey, Integer recordId) throws Exception { 354 | 355 | 356 | DeleteRecordRequest deleteRecordRequest = new DeleteRecordRequest().setDomain(domain).setRecordId(recordId); 357 | String jsonBody = getObjectMapper().writeValueAsString(deleteRecordRequest); 358 | 359 | TreeMap headerMap = TencentCloudAPITC3Singer.buildSignRequestHeaderWithBody(secretId, secretKey, DELETE_RECORD_ACTION, jsonBody); 360 | HttpHeaders headers = new HttpHeaders(); 361 | headers.setAll(headerMap); 362 | // 构建URI参数 363 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(PROTOCOL + HOST); 364 | // 构建RequestEntity 365 | RequestEntity requestEntity = new RequestEntity<>(jsonBody, headers, HttpMethod.POST, builder.build().toUri()); 366 | //发送请求 367 | ResponseEntity response = getRestTemplate().exchange(requestEntity, DeleteRecordResponse.class); 368 | return response.getBody(); 369 | } 370 | } 371 | --------------------------------------------------------------------------------