初始化1
This commit is contained in:
5
nginx-manager-backend/Dockerfile
Normal file
5
nginx-manager-backend/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM harbor.zgitm.com/library/eclipse-temurin:17-jre
|
||||
WORKDIR /app
|
||||
COPY target/*.jar app.jar
|
||||
EXPOSE 32402
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
116
nginx-manager-backend/pom.xml
Normal file
116
nginx-manager-backend/pom.xml
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.mokee</groupId>
|
||||
<artifactId>nginx-manager</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>Nginx Manager Backend</name>
|
||||
<description>Nginx域名转发管理平台 - Spring Boot 后端</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<!-- 默认激活的环境: qa -->
|
||||
<spring.profiles.active>qa</spring.profiles.active>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus (Spring Boot 3.x) -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- Maven 环境 Profiles -->
|
||||
<profiles>
|
||||
<!-- QA 测试环境 -->
|
||||
<profile>
|
||||
<id>qa</id>
|
||||
<properties>
|
||||
<spring.profiles.active>qa</spring.profiles.active>
|
||||
</properties>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
|
||||
<!-- PROD 生产环境 -->
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<spring.profiles.active>prod</spring.profiles.active>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<!-- Maven 资源过滤:将 pom 变量 @xxx@ 替换到 application.yml -->
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- 将 Maven profile 传给 Spring Boot -->
|
||||
<jvmArguments>-Dspring.profiles.active=${spring.profiles.active}</jvmArguments>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.mokee.nginx;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* Nginx域名转发管理平台 - Spring Boot 启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class NginxManagerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(NginxManagerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.mokee.nginx.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 跨域配置 — 允许网关域名的跨域请求
|
||||
*
|
||||
* 网关转发请求时 origin 是浏览器地址(非网关地址),
|
||||
* 业务后端必须开放跨域,否则浏览器拒绝响应。
|
||||
*
|
||||
* 注意:allowCredentials=true 时不能使用 allowedOrigins("*"),
|
||||
* 必须用 allowedOriginPatterns 指定具体模式。
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(
|
||||
"https://*.zgitm.com",
|
||||
"http://localhost:*"
|
||||
)
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mokee.nginx.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Jackson 配置 — 中文不转义为 Unicode
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
|
||||
return builder -> builder.featuresToDisable(
|
||||
JsonGenerator.Feature.ESCAPE_NON_ASCII
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.mokee.nginx.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.mokee.nginx.context.UserContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 替代 JPA 的 @PrePersist / @PreUpdate
|
||||
* 自动填充创建时间/更新时间/创建人/更新人
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, now);
|
||||
|
||||
String operator = resolveOperator();
|
||||
this.strictInsertFill(metaObject, "createdBy", String.class, operator);
|
||||
this.strictInsertFill(metaObject, "updatedBy", String.class, operator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
|
||||
|
||||
String operator = resolveOperator();
|
||||
this.strictUpdateFill(metaObject, "updatedBy", String.class, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 UserContext 获取当前操作人
|
||||
* 优先级:userName > userId > "系统"
|
||||
* 定时任务等无用户上下文的场景返回 "系统"
|
||||
*/
|
||||
private String resolveOperator() {
|
||||
try {
|
||||
String userName = UserContext.getUserName();
|
||||
if (userName != null && !userName.isEmpty()) return userName;
|
||||
String userId = UserContext.getUserId();
|
||||
if (userId != null && !userId.isEmpty()) return userId;
|
||||
} catch (Exception e) {
|
||||
// UserContext ThreadLocal 可能未初始化(定时任务、测试等场景)
|
||||
}
|
||||
return "系统";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.mokee.nginx.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* RestTemplate 配置,用于调用 Python API 和宝塔面板 API
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Value("${python-api.base-url}")
|
||||
private String pythonApiBaseUrl;
|
||||
|
||||
@Value("${python-api.connect-timeout:5000}")
|
||||
private int connectTimeout;
|
||||
|
||||
@Value("${python-api.read-timeout:30000}")
|
||||
private int readTimeout;
|
||||
|
||||
@Value("${python-api.cert-read-timeout:210000}")
|
||||
private int certReadTimeout;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.rootUri(pythonApiBaseUrl)
|
||||
.setConnectTimeout(Duration.ofMillis(connectTimeout))
|
||||
.setReadTimeout(Duration.ofMillis(readTimeout))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** 证书操作专用 RestTemplate(签发/续期超时: 5分钟) */
|
||||
@Bean
|
||||
public RestTemplate certRestTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.rootUri(pythonApiBaseUrl)
|
||||
.setConnectTimeout(Duration.ofMillis(connectTimeout))
|
||||
.setReadTimeout(Duration.ofMillis(certReadTimeout))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** 宝塔面板专用 RestTemplate(信任自签名证书,跳过证书校验) */
|
||||
@Bean
|
||||
public RestTemplate btRestTemplate() throws Exception {
|
||||
// 创建信任所有证书的 TrustManager
|
||||
TrustManager[] trustAll = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
}
|
||||
};
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustAll, new java.security.SecureRandom());
|
||||
|
||||
// 设置默认 SSL 工厂 + 跳过主机名验证
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
|
||||
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
|
||||
|
||||
// 使用 SimpleClientHttpRequestFactory(不依赖 Apache HttpClient)
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() {
|
||||
@Override
|
||||
protected void prepareConnection(HttpURLConnection connection, String httpMethod) {
|
||||
if (connection instanceof HttpsURLConnection httpsConn) {
|
||||
httpsConn.setSSLSocketFactory(sslContext.getSocketFactory());
|
||||
httpsConn.setHostnameVerifier((hostname, session) -> true);
|
||||
}
|
||||
}
|
||||
};
|
||||
factory.setConnectTimeout(Duration.ofMillis(connectTimeout));
|
||||
factory.setReadTimeout(Duration.ofMillis(readTimeout));
|
||||
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.mokee.nginx.context;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 用户上下文 — ThreadLocal 缓存网关注入的请求头
|
||||
*
|
||||
* 业务代码零侵入获取当前登录用户信息:
|
||||
* <pre>
|
||||
* String userId = UserContext.getUserId();
|
||||
* String userName = UserContext.getUserName();
|
||||
* </pre>
|
||||
*
|
||||
* 由 {@link com.mokee.nginx.filter.UserContextFilter} 在请求进入时设置,
|
||||
* 请求结束后自动清理。
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();
|
||||
|
||||
public static void set(Map<String, String> user) { CTX.set(user); }
|
||||
public static Map<String, String> get() {
|
||||
Map<String, String> map = CTX.get();
|
||||
return map != null ? map : Collections.emptyMap();
|
||||
}
|
||||
public static void clear() { CTX.remove(); }
|
||||
|
||||
// 快捷方法
|
||||
public static String getUserId() { return get().get("X-User-Id"); }
|
||||
public static String getUserName() { return get().get("X-User-Name"); }
|
||||
public static String getUserAccount() { return get().get("X-User-Account"); }
|
||||
public static String getUserEmail() { return get().get("X-User-Email"); }
|
||||
public static String getUserPost() { return get().get("X-User-Post"); }
|
||||
public static String getUserRoles() { return get().get("X-User-Roles"); }
|
||||
public static String getSystemCode() { return get().get("X-System-Code"); }
|
||||
public static String getSystemId() { return get().get("X-System-Id"); }
|
||||
public static String getSystemName() { return get().get("X-System-Name"); }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.mokee.nginx.controller;
|
||||
|
||||
import com.mokee.nginx.dto.ApiResponse;
|
||||
import com.mokee.nginx.entity.Certificate;
|
||||
import com.mokee.nginx.service.CertificateService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SSL证书管理 Controller
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/certificates")
|
||||
public class CertificateController {
|
||||
|
||||
@Autowired
|
||||
private CertificateService certificateService;
|
||||
|
||||
/**
|
||||
* 获取所有证书记录
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<Certificate>>> listCertificates() {
|
||||
log.info("获取证书列表");
|
||||
List<Certificate> certs = certificateService.listCertificates();
|
||||
return ResponseEntity.ok(ApiResponse.success(certs));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个证书详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Certificate>> getCertificate(@PathVariable Long id) {
|
||||
log.info("获取证书详情: id={}", id);
|
||||
Certificate cert = certificateService.getCertificate(id);
|
||||
return ResponseEntity.ok(ApiResponse.success(cert));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 证书签发请在宝塔面板上操作
|
||||
*/
|
||||
@PostMapping("/{id}/issue")
|
||||
public ResponseEntity<ApiResponse<Void>> issueCertificate(@PathVariable Long id) {
|
||||
log.warn("签发证书请求被拒绝(请到宝塔面板操作): id={}", id);
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "证书签发请在宝塔面板上操作(SSL → Let's Encrypt)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 证书续期请在宝塔面板上操作
|
||||
*/
|
||||
@PostMapping("/{id}/renew")
|
||||
public ResponseEntity<ApiResponse<Void>> renewCertificate(@PathVariable Long id) {
|
||||
log.warn("续期证书请求被拒绝(请到宝塔面板操作): id={}", id);
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "证书续期请在宝塔面板上操作(SSL → 续期)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知配置
|
||||
*/
|
||||
@PutMapping("/{id}/notify-config")
|
||||
public ResponseEntity<ApiResponse<Certificate>> updateNotifyConfig(
|
||||
@PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
log.info("更新通知配置: id={}", id);
|
||||
String notifyEmail = (String) body.get("notifyEmail");
|
||||
Integer notifyEnabled = body.get("notifyEnabled") != null
|
||||
? ((Number) body.get("notifyEnabled")).intValue() : null;
|
||||
Integer notifyDaysBefore = body.get("notifyDaysBefore") != null
|
||||
? ((Number) body.get("notifyDaysBefore")).intValue() : null;
|
||||
|
||||
Certificate cert = certificateService.updateNotifyConfig(
|
||||
id, notifyEmail, notifyEnabled, notifyDaysBefore);
|
||||
return ResponseEntity.ok(ApiResponse.success("通知配置已更新", cert));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.mokee.nginx.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.mokee.nginx.dto.ApiResponse;
|
||||
import com.mokee.nginx.entity.DnsZoneConfig;
|
||||
import com.mokee.nginx.mapper.DnsZoneConfigMapper;
|
||||
import com.mokee.nginx.service.DnsRecordService;
|
||||
import com.mokee.nginx.service.PythonApiClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DNS 解析管理 Controller
|
||||
* DNS 记录通过 DnsRecordService 管理(阿里云为权威数据源 + 本地审计追踪)
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/dns")
|
||||
public class DnsController {
|
||||
|
||||
@Autowired
|
||||
private DnsRecordService dnsRecordService;
|
||||
|
||||
@Autowired
|
||||
private DnsZoneConfigMapper zoneConfigMapper;
|
||||
|
||||
/**
|
||||
* 获取 DNS Zone 配置列表
|
||||
*/
|
||||
@GetMapping("/zones")
|
||||
public ResponseEntity<ApiResponse<List<DnsZoneConfig>>> listZones() {
|
||||
List<DnsZoneConfig> zones = zoneConfigMapper.selectList(null);
|
||||
return ResponseEntity.ok(ApiResponse.success(zones));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DNS 解析记录列表(含本地审计信息:创建人/时间/更新人/时间)
|
||||
*/
|
||||
@GetMapping("/records")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> listRecords(
|
||||
@RequestParam(defaultValue = "zgitm.com") String zone) {
|
||||
log.info("获取DNS记录: zone={}", zone);
|
||||
List<Map<String, Object>> records = dnsRecordService.listRecords(zone);
|
||||
return ResponseEntity.ok(ApiResponse.success(records));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 DNS 解析记录
|
||||
*/
|
||||
@PostMapping("/records")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> addRecord(
|
||||
@RequestBody Map<String, Object> body) {
|
||||
log.info("添加DNS记录: {}", body);
|
||||
Map<String, Object> result = dnsRecordService.addRecord(body);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 DNS 解析记录
|
||||
*/
|
||||
@DeleteMapping("/records/{recordId}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> deleteRecord(
|
||||
@PathVariable String recordId) {
|
||||
log.info("删除DNS记录: recordId={}", recordId);
|
||||
Map<String, Object> result = dnsRecordService.deleteRecord(recordId);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 DNS 解析记录
|
||||
*/
|
||||
@PutMapping("/records/{recordId}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> updateRecord(
|
||||
@PathVariable String recordId, @RequestBody Map<String, Object> body) {
|
||||
log.info("修改DNS记录: recordId={}, body={}", recordId, body);
|
||||
Map<String, Object> result = dnsRecordService.updateRecord(recordId, body);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 zone 配置(前端默认值用)
|
||||
*/
|
||||
@GetMapping("/default-zone")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getDefaultZone() {
|
||||
DnsZoneConfig zone = zoneConfigMapper.selectOne(
|
||||
new LambdaQueryWrapper<DnsZoneConfig>()
|
||||
.eq(DnsZoneConfig::getDomainZone, "zgitm.com")
|
||||
.eq(DnsZoneConfig::getStatus, 1));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
if (zone != null) {
|
||||
data.put("zone", zone.getDomainZone());
|
||||
data.put("defaultIp", zone.getDefaultIp());
|
||||
} else {
|
||||
data.put("zone", "zgitm.com");
|
||||
data.put("defaultIp", "10.20.1.160");
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success(data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.mokee.nginx.controller;
|
||||
|
||||
import com.mokee.nginx.dto.ApiResponse;
|
||||
import com.mokee.nginx.dto.RuleRequest;
|
||||
import com.mokee.nginx.entity.ForwardingRule;
|
||||
import com.mokee.nginx.service.ForwardingRuleService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 转发规则 REST Controller(只读模式)
|
||||
* 数据来源:宝塔面板 API
|
||||
* 新增/修改/删除请在宝塔面板上操作
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/rules")
|
||||
public class ForwardingRuleController {
|
||||
|
||||
@Autowired
|
||||
private ForwardingRuleService service;
|
||||
|
||||
/**
|
||||
* 获取所有转发规则列表(从宝塔面板拉取)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<ForwardingRule>>> listRules() {
|
||||
log.info("获取转发规则列表(数据源: 宝塔面板)");
|
||||
List<ForwardingRule> rules = service.listRules();
|
||||
return ResponseEntity.ok(ApiResponse.success(rules));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取规则详情(从当前列表中匹配)
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<ForwardingRule>> getRule(@PathVariable Long id) {
|
||||
log.info("获取规则详情: id={}", id);
|
||||
ForwardingRule rule = service.listRules().stream()
|
||||
.filter(r -> r.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("规则不存在: id=" + id));
|
||||
return ResponseEntity.ok(ApiResponse.success(rule));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 以下写操作已废弃 — 请在宝塔面板上操作
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Void>> addRule(@Valid @RequestBody RuleRequest request) {
|
||||
log.warn("新增规则请求被拒绝(请到宝塔面板操作): domain={}", request.getDomain());
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "新增转发规则请在宝塔面板上操作(站点 → 反向代理)"));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> updateRule(
|
||||
@PathVariable Long id, @Valid @RequestBody RuleRequest request) {
|
||||
log.warn("修改规则请求被拒绝(请到宝塔面板操作): id={}", id);
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "修改转发规则请在宝塔面板上操作(站点 → 反向代理)"));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteRule(@PathVariable Long id) {
|
||||
log.warn("删除规则请求被拒绝(请到宝塔面板操作): id={}", id);
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "删除转发规则请在宝塔面板上操作(站点 → 反向代理)"));
|
||||
}
|
||||
|
||||
@PostMapping("/reload")
|
||||
public ResponseEntity<ApiResponse<Void>> reloadNginx() {
|
||||
log.warn("重载Nginx请求被拒绝(请到宝塔面板操作)");
|
||||
return ResponseEntity.status(405).body(
|
||||
ApiResponse.error(405, "重载Nginx请在宝塔面板上操作"));
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<ApiResponse<String>> getStatus() {
|
||||
log.info("Nginx状态查询(已迁移至宝塔)");
|
||||
return ResponseEntity.ok(ApiResponse.success("Nginx 反代已迁移至宝塔面板管理"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则详情(配置文件内容 + SSL证书信息)
|
||||
*/
|
||||
@GetMapping("/{id}/config")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getConfig(@PathVariable Long id) {
|
||||
log.info("获取规则详情: id={}", id);
|
||||
Map<String, Object> result = service.getConfig(id);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/config")
|
||||
public ResponseEntity<ApiResponse<Void>> updateConfig(
|
||||
@PathVariable Long id, @RequestBody Object body) {
|
||||
return ResponseEntity.status(410).body(
|
||||
ApiResponse.error(410, "配置文件编辑已不可用,请前往宝塔面板"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/ssl")
|
||||
public ResponseEntity<ApiResponse<Void>> getSslFiles(@PathVariable Long id) {
|
||||
return ResponseEntity.status(410).body(
|
||||
ApiResponse.error(410, "SSL证书查看请前往宝塔面板"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/gzip")
|
||||
public ResponseEntity<ApiResponse<Void>> getGzipConfig(@PathVariable Long id) {
|
||||
return ResponseEntity.status(410).body(
|
||||
ApiResponse.error(410, "内容压缩配置请前往宝塔面板查看"));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/gzip")
|
||||
public ResponseEntity<ApiResponse<Void>> updateGzipConfig(
|
||||
@PathVariable Long id, @RequestBody Object body) {
|
||||
return ResponseEntity.status(410).body(
|
||||
ApiResponse.error(410, "内容压缩配置请前往宝塔面板修改"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/logs")
|
||||
public ResponseEntity<ApiResponse<Void>> getDomainLogs(
|
||||
@PathVariable Long id, @RequestParam(defaultValue = "200") int lines) {
|
||||
return ResponseEntity.status(410).body(
|
||||
ApiResponse.error(410, "日志查看请前往宝塔面板"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.mokee.nginx.controller;
|
||||
|
||||
import com.mokee.nginx.dto.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 参数校验失败
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败: {}", message);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleRuntimeException(RuntimeException ex) {
|
||||
log.error("业务异常: {}", ex.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error(ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 其他未捕获异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
|
||||
log.error("系统异常: ", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error("系统内部错误: " + ex.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.mokee.nginx.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一响应结构
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.code(0)
|
||||
.message("success")
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.code(0)
|
||||
.message(message)
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.data(null)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.code(-1)
|
||||
.message(message)
|
||||
.data(null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.mokee.nginx.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 新增/修改转发规则请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class RuleRequest {
|
||||
|
||||
/** 域名 */
|
||||
@NotBlank(message = "域名不能为空")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9]([a-zA-Z0-9\\-\\.]*[a-zA-Z0-9])?$",
|
||||
message = "域名格式不正确")
|
||||
private String domain;
|
||||
|
||||
/** 协议类型: http 或 https */
|
||||
@NotBlank(message = "协议类型不能为空")
|
||||
@Pattern(regexp = "^(http|https)$", message = "协议类型必须为 http 或 https")
|
||||
private String protocol;
|
||||
|
||||
/** 转发目标地址 */
|
||||
@NotBlank(message = "转发目标地址不能为空")
|
||||
@Pattern(regexp = "^https?://.+", message = "转发目标地址必须以 http:// 或 https:// 开头")
|
||||
private String target;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.mokee.nginx.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SSL证书实体
|
||||
*/
|
||||
@TableName("certificates")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Certificate {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 域名 */
|
||||
@TableField("domain")
|
||||
private String domain;
|
||||
|
||||
/** 证书类型: internal_ca / letsencrypt */
|
||||
@TableField("cert_type")
|
||||
@Builder.Default
|
||||
private String certType = "internal_ca";
|
||||
|
||||
/** 证书文件路径 */
|
||||
@TableField("cert_path")
|
||||
private String certPath;
|
||||
|
||||
/** 私钥文件路径 */
|
||||
@TableField("key_path")
|
||||
private String keyPath;
|
||||
|
||||
/** 签发日期 */
|
||||
@TableField("issue_date")
|
||||
private LocalDateTime issueDate;
|
||||
|
||||
/** 过期日期 */
|
||||
@TableField("expire_date")
|
||||
private LocalDateTime expireDate;
|
||||
|
||||
/** 状态: active / expired / revoked */
|
||||
@TableField("status")
|
||||
@Builder.Default
|
||||
private String status = "active";
|
||||
|
||||
/** 通知收件人邮箱 */
|
||||
@TableField("notify_email")
|
||||
private String notifyEmail;
|
||||
|
||||
/** 是否启用到期通知 */
|
||||
@TableField("notify_enabled")
|
||||
@Builder.Default
|
||||
private Integer notifyEnabled = 1;
|
||||
|
||||
/** 提前多少天开始通知 */
|
||||
@TableField("notify_days_before")
|
||||
@Builder.Default
|
||||
private Integer notifyDaysBefore = 15;
|
||||
|
||||
/** 上次续期时间 */
|
||||
@TableField("last_renew_time")
|
||||
private LocalDateTime lastRenewTime;
|
||||
|
||||
/** 续期/签发日志 */
|
||||
@TableField("renew_log")
|
||||
private String renewLog;
|
||||
|
||||
/** 创建人 */
|
||||
@TableField(value = "created_by", fill = FieldFill.INSERT)
|
||||
private String createdBy;
|
||||
|
||||
/** 更新人 */
|
||||
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updatedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.mokee.nginx.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DNS 解析记录本地审计实体
|
||||
* 阿里云 DNS 为权威数据源,本表仅做操作审计追踪
|
||||
*/
|
||||
@TableName("dns_records")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DnsRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 阿里云解析记录ID */
|
||||
@TableField("record_id")
|
||||
private String recordId;
|
||||
|
||||
/** 主域名(如 zgitm.com) */
|
||||
@TableField("domain_zone")
|
||||
private String domainZone;
|
||||
|
||||
/** 主机记录 */
|
||||
@TableField("rr")
|
||||
private String rr;
|
||||
|
||||
/** 记录类型: A / CNAME / TXT */
|
||||
@TableField("record_type")
|
||||
private String recordType;
|
||||
|
||||
/** 记录值(IP 或域名) */
|
||||
@TableField("record_value")
|
||||
private String recordValue;
|
||||
|
||||
/** TTL 生效时间(秒) */
|
||||
@TableField("ttl")
|
||||
@Builder.Default
|
||||
private Integer ttl = 600;
|
||||
|
||||
/** 状态: 1-正常, 0-已删除(逻辑删除) */
|
||||
@TableField("status")
|
||||
@TableLogic(value = "1", delval = "0")
|
||||
@Builder.Default
|
||||
private Integer status = 1;
|
||||
|
||||
/** 创建人 */
|
||||
@TableField(value = "created_by", fill = FieldFill.INSERT)
|
||||
private String createdBy;
|
||||
|
||||
/** 更新人 */
|
||||
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updatedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.mokee.nginx.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DNS Zone 配置实体
|
||||
*/
|
||||
@TableName("dns_zone_config")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DnsZoneConfig {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 主域名 (如 zgitm.com) */
|
||||
@TableField("domain_zone")
|
||||
private String domainZone;
|
||||
|
||||
/** 默认解析 IP */
|
||||
@TableField("default_ip")
|
||||
@Builder.Default
|
||||
private String defaultIp = "10.20.1.160";
|
||||
|
||||
/** DNS 服务商 */
|
||||
@TableField("dns_provider")
|
||||
@Builder.Default
|
||||
private String dnsProvider = "aliyun";
|
||||
|
||||
/** 状态: 1-启用 0-停用 */
|
||||
@TableField("status")
|
||||
@TableLogic(value = "1", delval = "0")
|
||||
@Builder.Default
|
||||
private Integer status = 1;
|
||||
|
||||
/** 创建人 */
|
||||
@TableField(value = "created_by", fill = FieldFill.INSERT)
|
||||
private String createdBy;
|
||||
|
||||
/** 更新人 */
|
||||
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updatedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.mokee.nginx.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Nginx转发规则实体
|
||||
*/
|
||||
@TableName("forwarding_rules")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ForwardingRule {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 域名 */
|
||||
@TableField("domain")
|
||||
private String domain;
|
||||
|
||||
/** 协议类型: http / https */
|
||||
@TableField("protocol")
|
||||
private String protocol;
|
||||
|
||||
/** 转发目标地址 (http://host:port 或 https://host:port) */
|
||||
@TableField("target")
|
||||
private String target;
|
||||
|
||||
/** Nginx配置文件路径 */
|
||||
@TableField("config_file")
|
||||
private String configFile;
|
||||
|
||||
/** SSL证书路径 */
|
||||
@TableField("ssl_cert")
|
||||
private String sslCert;
|
||||
|
||||
/** SSL私钥路径 */
|
||||
@TableField("ssl_key")
|
||||
private String sslKey;
|
||||
|
||||
/** gzip压缩开关: true-开启, false-关闭 */
|
||||
@TableField("gzip_enabled")
|
||||
@Builder.Default
|
||||
private Boolean gzipEnabled = false;
|
||||
|
||||
/** gzip压缩MIME类型(空格分隔) */
|
||||
@TableField("gzip_types")
|
||||
@Builder.Default
|
||||
private String gzipTypes = "text/plain text/css text/xml text/javascript application/x-javascript application/json application/xml application/xml+rss application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype";
|
||||
|
||||
/** gzip压缩级别 1-9 */
|
||||
@TableField("gzip_comp_level")
|
||||
@Builder.Default
|
||||
private Integer gzipCompLevel = 6;
|
||||
|
||||
/** gzip最小压缩长度(可带k/m单位,如 "1000", "1k", "2m") */
|
||||
@TableField("gzip_min_length")
|
||||
@Builder.Default
|
||||
private String gzipMinLength = "1000";
|
||||
|
||||
/** 状态: 1-正常, 0-已删除 */
|
||||
@TableField("status")
|
||||
@TableLogic(value = "1", delval = "0")
|
||||
@Builder.Default
|
||||
private Integer status = 1;
|
||||
|
||||
/** 创建人 */
|
||||
@TableField(value = "created_by", fill = FieldFill.INSERT)
|
||||
private String createdBy;
|
||||
|
||||
/** 更新人 */
|
||||
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updatedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.mokee.nginx.filter;
|
||||
|
||||
import com.mokee.nginx.context.UserContext;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 用户上下文过滤器 — 提取网关注入的请求头并缓存到 ThreadLocal
|
||||
*
|
||||
* 网关验证 Token 后在转发请求时注入以下请求头(可信任,无需再鉴权):
|
||||
* X-User-Id, X-User-Name, X-User-Account, X-User-Email,
|
||||
* X-User-Post, X-User-Roles, X-System-Id, X-System-Code, X-System-Name
|
||||
*/
|
||||
@Component
|
||||
public class UserContextFilter implements Filter {
|
||||
|
||||
private static final String[] HEADERS = {
|
||||
"X-User-Id", "X-User-Name", "X-User-Account",
|
||||
"X-User-Email", "X-User-Post", "X-User-Roles",
|
||||
"X-System-Id", "X-System-Code", "X-System-Name"
|
||||
};
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
Map<String, String> user = new HashMap<>();
|
||||
for (String h : HEADERS) {
|
||||
String v = request.getHeader(h);
|
||||
if (v != null) {
|
||||
// 网关部分请求头值可能被 URL 编码(如中文用户名),解码后再存入
|
||||
try {
|
||||
v = URLDecoder.decode(v, StandardCharsets.UTF_8);
|
||||
} catch (Exception ignored) { }
|
||||
user.put(h, v);
|
||||
}
|
||||
}
|
||||
UserContext.set(user);
|
||||
try {
|
||||
chain.doFilter(req, res);
|
||||
} finally {
|
||||
// 请求结束,清理 ThreadLocal 防止内存泄漏
|
||||
UserContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.nginx.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.nginx.entity.Certificate;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* SSL证书 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CertificateMapper extends BaseMapper<Certificate> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.nginx.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.nginx.entity.DnsRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* DNS 记录本地审计 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface DnsRecordMapper extends BaseMapper<DnsRecord> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.nginx.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.nginx.entity.DnsZoneConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* DNS Zone 配置 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface DnsZoneConfigMapper extends BaseMapper<DnsZoneConfig> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.nginx.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.nginx.entity.ForwardingRule;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* Nginx转发规则 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ForwardingRuleMapper extends BaseMapper<ForwardingRule> {
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 宝塔面板 API HTTP 客户端(只读)
|
||||
*
|
||||
* 接口清单(来自宝塔官方 API 文档 api-doc.pdf):
|
||||
* - 站点列表: POST /data?action=getData&table=sites (参数: p, limit, type=-1)
|
||||
* - 反代列表: POST /site?action=GetProxyList (参数: sitename)
|
||||
* - 证书订单: POST /acme?action=get_orders
|
||||
*
|
||||
* 签名规则:request_token = MD5(request_time + MD5(api_key))
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BTPanelApiClient {
|
||||
|
||||
@Value("${bt-panel.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${bt-panel.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
private final RestTemplate btRestTemplate;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public BTPanelApiClient(
|
||||
@org.springframework.beans.factory.annotation.Qualifier("btRestTemplate") RestTemplate btRestTemplate) {
|
||||
this.btRestTemplate = btRestTemplate;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 签名工具
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
private String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("MD5计算失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求到宝塔 API(参数通过 URL query string 传递)
|
||||
*/
|
||||
private Map<String, Object> postRequest(String route, String action,
|
||||
MultiValueMap<String, String> extraParams) {
|
||||
String url = buildUrl(route, action, extraParams);
|
||||
log.debug("宝塔API请求: POST {}", url);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
// 发送空 body(参数都在URL里)
|
||||
HttpEntity<String> entity = new HttpEntity<>("", headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = btRestTemplate.exchange(
|
||||
url, HttpMethod.POST, entity,
|
||||
new ParameterizedTypeReference<>() {});
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("宝塔API请求失败: url={}, error={}", url, e.getMessage());
|
||||
throw new RuntimeException("宝塔API请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求,返回列表
|
||||
*/
|
||||
private List<Map<String, Object>> postRequestForList(String route, String action,
|
||||
MultiValueMap<String, String> extraParams) {
|
||||
String url = buildUrl(route, action, extraParams);
|
||||
log.debug("宝塔API请求(List): POST {}", url);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
HttpEntity<String> entity = new HttpEntity<>("", headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<List<Map<String, Object>>> response = btRestTemplate.exchange(
|
||||
url, HttpMethod.POST, entity,
|
||||
new ParameterizedTypeReference<>() {});
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("宝塔API请求失败(List): url={}, error={}", url, e.getMessage());
|
||||
throw new RuntimeException("宝塔API请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带签名的 URL
|
||||
*/
|
||||
private String buildUrl(String route, String action,
|
||||
MultiValueMap<String, String> extraParams) {
|
||||
String requestTime = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
String rawToken = requestTime + md5(apiKey);
|
||||
String requestToken = md5(rawToken);
|
||||
|
||||
StringBuilder url = new StringBuilder(baseUrl);
|
||||
url.append(route);
|
||||
url.append("?action=").append(action);
|
||||
url.append("&request_time=").append(requestTime);
|
||||
url.append("&request_token=").append(requestToken);
|
||||
|
||||
if (extraParams != null) {
|
||||
for (Map.Entry<String, List<String>> entry : extraParams.entrySet()) {
|
||||
for (String value : entry.getValue()) {
|
||||
url.append("&").append(entry.getKey()).append("=").append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 业务接口
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 获取所有站点列表
|
||||
* API: POST /data?action=getData&table=sites
|
||||
*
|
||||
* 返回: {"data": [{"id":12,"name":"xxx.com","status":"1","project_type":"proxy",...}]}
|
||||
*/
|
||||
public Map<String, Object> getSiteList() {
|
||||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
params.add("table", "sites");
|
||||
params.add("p", "1");
|
||||
params.add("limit", "200");
|
||||
params.add("type", "-1");
|
||||
|
||||
Map<String, Object> result = postRequest("/data", "getData", params);
|
||||
log.info("宝塔API: 获取站点列表成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定站点的反向代理列表
|
||||
* API: POST /site?action=GetProxyList
|
||||
*
|
||||
* 参数: sitename=域名
|
||||
* 返回: [{"proxyname":"...","proxydir":"/","proxysite":"http://...",...}]
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Map<String, Object>> getProxyList(String siteName) {
|
||||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
params.add("sitename", siteName);
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> result = postRequestForList("/site", "GetProxyList", params);
|
||||
log.debug("宝塔API: 获取站点 {} 反代列表成功, 数量={}", siteName,
|
||||
result != null ? result.size() : 0);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取宝塔站点 {} 反代列表失败: {}", siteName, e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ACME 证书订单列表
|
||||
* API: POST /acme?action=get_orders
|
||||
*
|
||||
* 返回: [{"domains":["xxx.com"],"expires":1780111135,"status":"valid",...}]
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Map<String, Object>> getCertOrders() {
|
||||
try {
|
||||
List<Map<String, Object>> result = postRequestForList("/acme", "get_orders", null);
|
||||
log.info("宝塔API: 获取证书订单列表成功, 数量={}", result != null ? result.size() : 0);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("获取宝塔证书订单列表失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取宝塔证书订单列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取宝塔服务器上的文件内容
|
||||
* API: POST /files?action=GetFileBody
|
||||
*
|
||||
* @param filePath 文件绝对路径 (如 /www/server/panel/vhost/nginx/xxx.com.conf)
|
||||
* @return 文件内容字符串
|
||||
*/
|
||||
public String getFileContent(String filePath) {
|
||||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
params.add("path", filePath);
|
||||
|
||||
Map<String, Object> result = postRequest("/files", "GetFileBody", params);
|
||||
if (result != null && result.get("data") != null) {
|
||||
return (String) result.get("data");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名的 Nginx 配置文件内容
|
||||
*/
|
||||
public String getNginxConfig(String domain) {
|
||||
String configPath = "/www/server/panel/vhost/nginx/" + domain + ".conf";
|
||||
try {
|
||||
return getFileContent(configPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 {} 的 nginx 配置失败: {}", domain, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.mokee.nginx.entity.Certificate;
|
||||
import com.mokee.nginx.mapper.CertificateMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 证书到期邮件通知调度器
|
||||
* 每天 10:00 检查 Let's Encrypt 证书,到期前 N 天起连续发邮件提醒
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CertNotifyScheduler {
|
||||
|
||||
@Autowired
|
||||
private CertificateMapper certificateMapper;
|
||||
|
||||
/** 网关 API 地址(可配置,不用 rootUri 的 RestTemplate) */
|
||||
private final RestTemplate gatewayRestTemplate;
|
||||
|
||||
@Value("${gateway.base-url:https://gw.server.mokee.com}")
|
||||
private String gatewayBaseUrl;
|
||||
|
||||
@Value("${gateway.system-code:nginxServer}")
|
||||
private String systemCode;
|
||||
|
||||
public CertNotifyScheduler() {
|
||||
this.gatewayRestTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天 10:00 执行证书到期检查并发送邮件
|
||||
*/
|
||||
@Scheduled(cron = "0 0 10 * * ?")
|
||||
public void checkAndNotify() {
|
||||
log.info("=== 证书到期通知检查开始 ===");
|
||||
|
||||
List<Certificate> certs = certificateMapper.selectList(
|
||||
new LambdaQueryWrapper<Certificate>()
|
||||
.eq(Certificate::getCertType, "letsencrypt")
|
||||
.eq(Certificate::getStatus, "active")
|
||||
.eq(Certificate::getNotifyEnabled, 1)
|
||||
.isNotNull(Certificate::getNotifyEmail));
|
||||
|
||||
if (certs.isEmpty()) {
|
||||
log.info("无需通知的 Let's Encrypt 证书");
|
||||
return;
|
||||
}
|
||||
|
||||
String token = null;
|
||||
int notifiedCount = 0;
|
||||
|
||||
for (Certificate cert : certs) {
|
||||
if (cert.getExpireDate() == null) {
|
||||
log.warn("{}: 无过期日期信息,跳过", cert.getDomain());
|
||||
continue;
|
||||
}
|
||||
|
||||
long daysLeft = ChronoUnit.DAYS.between(
|
||||
LocalDate.now(),
|
||||
cert.getExpireDate().toLocalDate());
|
||||
|
||||
log.info("{}: 剩余 {} 天到期 (阈值: {}天前开始)",
|
||||
cert.getDomain(), daysLeft, cert.getNotifyDaysBefore());
|
||||
|
||||
// 判断是否在通知窗口:到期前 notifyDaysBefore 天 ~ 到期前1天
|
||||
if (daysLeft > cert.getNotifyDaysBefore() || daysLeft <= 0) {
|
||||
log.info("{}: 不在通知窗口内 (daysLeft={})", cert.getDomain(), daysLeft);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取 Token
|
||||
if (token == null) {
|
||||
token = fetchToken();
|
||||
}
|
||||
if (token == null) {
|
||||
log.error("无法获取网关 Token,跳过 {}", cert.getDomain());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
String subject = String.format("⚠️ SSL证书到期提醒 - %s (剩余%d天)",
|
||||
cert.getDomain(), daysLeft);
|
||||
String content = buildEmailHtml(cert.getDomain(), daysLeft, cert.getExpireDate());
|
||||
|
||||
boolean success = sendEmail(token, cert.getNotifyEmail(), subject, content);
|
||||
if (success) {
|
||||
token = null; // Token 已消费,下次需重新获取
|
||||
notifiedCount++;
|
||||
log.info("{}: 邮件发送成功 → {}", cert.getDomain(), cert.getNotifyEmail());
|
||||
} else {
|
||||
log.warn("{}: 邮件发送失败,Token 未消费可重试", cert.getDomain());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("=== 证书到期通知完成: 发送 {} 封邮件 ===", notifiedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网关获取一次性 Token
|
||||
*/
|
||||
private String fetchToken() {
|
||||
try {
|
||||
String url = gatewayBaseUrl + "/zgapi/v1/open/token";
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("systemCode", systemCode);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<Map> response = gatewayRestTemplate.postForEntity(
|
||||
url, request, Map.class);
|
||||
|
||||
Map<String, Object> data = (Map<String, Object>) response.getBody();
|
||||
if (data != null && Integer.valueOf(200).equals(data.get("code"))) {
|
||||
Map<String, Object> inner = (Map<String, Object>) data.get("data");
|
||||
String token = (String) inner.get("token");
|
||||
log.info("获取 Token 成功: {}", systemCode);
|
||||
return token;
|
||||
} else {
|
||||
log.error("获取 Token 失败: {}", data);
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取 Token 异常: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过网关发送邮件
|
||||
*/
|
||||
private boolean sendEmail(String token, String to, String subject, String content) {
|
||||
try {
|
||||
String url = gatewayBaseUrl + "/zgapi/v1/open/email/send";
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("to", to);
|
||||
body.put("subject", subject);
|
||||
body.put("content", content);
|
||||
body.put("contentType", "html");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(token);
|
||||
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<Map> response = gatewayRestTemplate.postForEntity(
|
||||
url, request, Map.class);
|
||||
|
||||
Map<String, Object> data = response.getBody();
|
||||
return data != null && Integer.valueOf(200).equals(data.get("code"));
|
||||
} catch (Exception e) {
|
||||
log.error("发送邮件异常: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 HTML 邮件内容
|
||||
*/
|
||||
private String buildEmailHtml(String domain, long daysLeft, LocalDateTime expireDate) {
|
||||
String color;
|
||||
String level;
|
||||
if (daysLeft <= 5) {
|
||||
color = "#F56C6C"; level = "危险";
|
||||
} else if (daysLeft <= 10) {
|
||||
color = "#E6A23C"; level = "警告";
|
||||
} else {
|
||||
color = "#409EFF"; level = "提醒";
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"<!DOCTYPE html>" +
|
||||
"<html><body style=\"font-family:'Microsoft YaHei',Arial,sans-serif;\">" +
|
||||
"<div style=\"max-width:600px;margin:0 auto;padding:20px;border:1px solid #EBEEF5;border-radius:8px;\">" +
|
||||
"<div style=\"background:%s;padding:16px;border-radius:8px 8px 0 0;text-align:center;\">" +
|
||||
"<h2 style=\"color:#fff;margin:0;\">⚠️ SSL证书即将到期 [%s]</h2></div>" +
|
||||
"<div style=\"padding:20px;\">" +
|
||||
"<table style=\"width:100%%;border-collapse:collapse;\">" +
|
||||
"<tr><td style=\"padding:8px 0;color:#909399;width:80px;\">域名</td>" +
|
||||
"<td style=\"padding:8px 0;font-weight:bold;font-size:16px;\">%s</td></tr>" +
|
||||
"<tr><td style=\"padding:8px 0;color:#909399;\">到期时间</td>" +
|
||||
"<td style=\"padding:8px 0;\">%s</td></tr>" +
|
||||
"<tr><td style=\"padding:8px 0;color:#909399;\">剩余天数</td>" +
|
||||
"<td style=\"padding:8px 0;color:%s;font-weight:bold;font-size:20px;\">%d 天</td></tr>" +
|
||||
"<tr><td style=\"padding:8px 0;color:#909399;\">证书类型</td>" +
|
||||
"<td style=\"padding:8px 0;\">Let's Encrypt (阿里云DNS验证)</td></tr>" +
|
||||
"</table>" +
|
||||
"<hr style=\"border:0;border-top:1px solid #EBEEF5;margin:16px 0;\">" +
|
||||
"<p style=\"color:#909399;font-size:13px;\">系统已配置 acme.sh 自动续期。" +
|
||||
"如续期失败请检查阿里云 AccessKey 和网络连通性。</p>" +
|
||||
"<p style=\"color:#C0C4CC;font-size:12px;\">此邮件由 Nginx域名管理平台自动发送</p>" +
|
||||
"</div></div></body></html>",
|
||||
color, level, domain,
|
||||
expireDate.toLocalDate().toString(),
|
||||
color, daysLeft);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.mokee.nginx.entity.Certificate;
|
||||
import com.mokee.nginx.mapper.CertificateMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* SSL证书管理服务(只读模式)
|
||||
* 数据来源:宝塔面板 ACME 证书订单 API
|
||||
* 签发/续期请在宝塔面板上操作
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CertificateService {
|
||||
|
||||
@Autowired
|
||||
private CertificateMapper certificateMapper;
|
||||
|
||||
@Autowired
|
||||
private BTPanelApiClient btPanelApiClient;
|
||||
|
||||
/**
|
||||
* 获取所有证书列表(宝塔数据 + 本地通知配置合并)
|
||||
*/
|
||||
public List<Certificate> listCertificates() {
|
||||
List<Certificate> certs = new ArrayList<>();
|
||||
long index = 1;
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> orders = btPanelApiClient.getCertOrders();
|
||||
|
||||
if (orders == null || orders.isEmpty()) {
|
||||
log.info("宝塔证书订单列表为空");
|
||||
return certs;
|
||||
}
|
||||
|
||||
log.info("宝塔证书订单数量: {}", orders.size());
|
||||
|
||||
for (Map<String, Object> order : orders) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> domains = (List<String>) order.get("domains");
|
||||
String domain = (domains != null && !domains.isEmpty())
|
||||
? domains.get(0) : "未知域名";
|
||||
|
||||
// 过期时间(Unix时间戳)
|
||||
Object expiresObj = order.get("cert_timeout");
|
||||
if (expiresObj == null) {
|
||||
expiresObj = order.get("expires");
|
||||
}
|
||||
LocalDateTime expireDate = null;
|
||||
if (expiresObj instanceof Number) {
|
||||
long expiresTs = ((Number) expiresObj).longValue();
|
||||
expireDate = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochSecond(expiresTs), ZoneId.of("Asia/Shanghai"));
|
||||
}
|
||||
|
||||
String orderStatus = String.valueOf(order.getOrDefault("status", "unknown"));
|
||||
String status = "valid".equals(orderStatus) ? "active" : orderStatus;
|
||||
|
||||
// 查找本地数据库中该域名的通知配置
|
||||
Certificate localCert = certificateMapper.selectOne(
|
||||
new LambdaQueryWrapper<Certificate>()
|
||||
.eq(Certificate::getDomain, domain));
|
||||
|
||||
Certificate cert = Certificate.builder()
|
||||
.domain(domain)
|
||||
.certType("letsencrypt")
|
||||
.status(status)
|
||||
.expireDate(expireDate)
|
||||
.build();
|
||||
|
||||
// 合并本地通知配置
|
||||
if (localCert != null) {
|
||||
cert.setId(localCert.getId());
|
||||
cert.setNotifyEmail(localCert.getNotifyEmail());
|
||||
cert.setNotifyEnabled(localCert.getNotifyEnabled());
|
||||
cert.setNotifyDaysBefore(localCert.getNotifyDaysBefore());
|
||||
} else {
|
||||
cert.setId(index);
|
||||
cert.setNotifyEnabled(0); // 默认未配置通知
|
||||
}
|
||||
index++;
|
||||
|
||||
certs.add(cert);
|
||||
}
|
||||
|
||||
log.info("从宝塔获取到 {} 条证书记录(已合并本地通知配置)", certs.size());
|
||||
} catch (Exception e) {
|
||||
log.error("从宝塔获取证书列表失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取证书列表失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return certs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个证书详情
|
||||
*/
|
||||
public Certificate getCertificate(Long id) {
|
||||
// 从列表中匹配(宝塔数据源下id是序号,非持久化)
|
||||
List<Certificate> allCerts = listCertificates();
|
||||
return allCerts.stream()
|
||||
.filter(c -> c.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("证书不存在: id=" + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知配置(本地数据库操作,不影响宝塔)
|
||||
*/
|
||||
@Transactional
|
||||
public Certificate updateNotifyConfig(Long id, String notifyEmail,
|
||||
Integer notifyEnabled, Integer notifyDaysBefore) {
|
||||
// 通知配置存本地数据库,按域名匹配
|
||||
List<Certificate> allCerts = listCertificates();
|
||||
Certificate btCert = allCerts.stream()
|
||||
.filter(c -> c.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("证书不存在: id=" + id));
|
||||
|
||||
// 查找或创建本地通知配置记录
|
||||
Certificate localCert = certificateMapper.selectOne(
|
||||
new LambdaQueryWrapper<Certificate>()
|
||||
.eq(Certificate::getDomain, btCert.getDomain()));
|
||||
|
||||
if (localCert == null) {
|
||||
localCert = new Certificate();
|
||||
localCert.setDomain(btCert.getDomain());
|
||||
localCert.setCertType(btCert.getCertType());
|
||||
localCert.setStatus(btCert.getStatus());
|
||||
localCert.setExpireDate(btCert.getExpireDate());
|
||||
}
|
||||
|
||||
if (notifyEmail != null) {
|
||||
localCert.setNotifyEmail(notifyEmail);
|
||||
}
|
||||
if (notifyEnabled != null) {
|
||||
localCert.setNotifyEnabled(notifyEnabled);
|
||||
}
|
||||
if (notifyDaysBefore != null) {
|
||||
if (notifyDaysBefore < 1 || notifyDaysBefore > 30) {
|
||||
throw new RuntimeException("通知天数必须在 1~30 之间");
|
||||
}
|
||||
localCert.setNotifyDaysBefore(notifyDaysBefore);
|
||||
}
|
||||
|
||||
if (localCert.getId() != null) {
|
||||
certificateMapper.updateById(localCert);
|
||||
} else {
|
||||
certificateMapper.insert(localCert);
|
||||
}
|
||||
|
||||
log.info("通知配置已更新: domain={}, email={}, enabled={}, daysBefore={}",
|
||||
localCert.getDomain(), localCert.getNotifyEmail(),
|
||||
localCert.getNotifyEnabled(), localCert.getNotifyDaysBefore());
|
||||
return localCert;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 以下写操作已废弃 — 请在宝塔面板上操作
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @deprecated 证书签发请在宝塔面板上操作
|
||||
*/
|
||||
@Deprecated
|
||||
public Certificate issueCertificate(Long id) {
|
||||
throw new UnsupportedOperationException(
|
||||
"证书签发请在宝塔面板上操作(SSL → Let's Encrypt)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 证书续期请在宝塔面板上操作
|
||||
*/
|
||||
@Deprecated
|
||||
public Certificate renewCertificate(Long id) {
|
||||
throw new UnsupportedOperationException(
|
||||
"证书续期请在宝塔面板上操作(SSL → 续期)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 证书记录同步已不再需要(数据源为宝塔API)
|
||||
*/
|
||||
@Deprecated
|
||||
public void syncCertificateRecord(String domain, String certPath, String keyPath) {
|
||||
log.info("syncCertificateRecord 已废弃(数据源已切换至宝塔API): domain={}", domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 缓存失效已不再需要(数据源为宝塔API)
|
||||
*/
|
||||
@Deprecated
|
||||
public void invalidateCache() {
|
||||
log.info("invalidateCache 已废弃(数据源已切换至宝塔API)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.mokee.nginx.entity.DnsRecord;
|
||||
import com.mokee.nginx.mapper.DnsRecordMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* DNS 记录业务层 — 阿里云 DNS 为权威数据源,本地 dns_records 表仅做操作审计
|
||||
*
|
||||
* 核心流程:
|
||||
* - 列表查询:阿里云 API → 本地表按 record_id 合并审计字段 → 返回富化结果
|
||||
* - 新增/修改/删除:先操作阿里云 → 再同步本地审计表
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DnsRecordService {
|
||||
|
||||
@Autowired
|
||||
private DnsRecordMapper dnsRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private PythonApiClient pythonApiClient;
|
||||
|
||||
/**
|
||||
* 获取 DNS 解析记录列表(含本地审计信息)
|
||||
*
|
||||
* @param zone 主域名,如 zgitm.com
|
||||
* @return 富化后的记录列表,每条包含 createdBy/createdAt/updatedBy/updatedAt
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Map<String, Object>> listRecords(String zone) {
|
||||
// 1. 从阿里云获取权威 DNS 记录
|
||||
Map<String, Object> aliyunResult = pythonApiClient.listDnsRecords(zone);
|
||||
Object dataObj = aliyunResult.get("data");
|
||||
List<Map<String, Object>> aliyunRecords;
|
||||
if (dataObj instanceof List) {
|
||||
aliyunRecords = (List<Map<String, Object>>) dataObj;
|
||||
} else if (dataObj instanceof Map) {
|
||||
// 兼容 Python API 返回格式:{ data: { data: [...] } }
|
||||
Object inner = ((Map<String, Object>) dataObj).get("data");
|
||||
if (inner instanceof List) {
|
||||
aliyunRecords = (List<Map<String, Object>>) inner;
|
||||
} else {
|
||||
aliyunRecords = new ArrayList<>();
|
||||
}
|
||||
} else {
|
||||
aliyunRecords = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (aliyunRecords.isEmpty()) {
|
||||
return aliyunRecords;
|
||||
}
|
||||
|
||||
// 2. 查询本地审计表
|
||||
List<String> recordIds = aliyunRecords.stream()
|
||||
.map(r -> String.valueOf(r.get("record_id")))
|
||||
.filter(id -> !id.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, DnsRecord> auditMap = new HashMap<>();
|
||||
if (!recordIds.isEmpty()) {
|
||||
List<DnsRecord> localRecords = dnsRecordMapper.selectList(
|
||||
new LambdaQueryWrapper<DnsRecord>()
|
||||
.in(DnsRecord::getRecordId, recordIds)
|
||||
.eq(DnsRecord::getStatus, 1));
|
||||
auditMap = localRecords.stream()
|
||||
.collect(Collectors.toMap(DnsRecord::getRecordId, r -> r, (a, b) -> a));
|
||||
}
|
||||
|
||||
// 3. 合并审计字段
|
||||
for (Map<String, Object> record : aliyunRecords) {
|
||||
String recordId = String.valueOf(record.get("record_id"));
|
||||
DnsRecord audit = auditMap.get(recordId);
|
||||
if (audit != null) {
|
||||
record.put("createdBy", audit.getCreatedBy());
|
||||
record.put("createdAt", audit.getCreatedAt());
|
||||
record.put("updatedBy", audit.getUpdatedBy());
|
||||
record.put("updatedAt", audit.getUpdatedAt());
|
||||
} else {
|
||||
record.put("createdBy", null);
|
||||
record.put("createdAt", null);
|
||||
record.put("updatedBy", null);
|
||||
record.put("updatedAt", null);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 按创建时间倒序排列(有审计数据的优先)
|
||||
aliyunRecords.sort((a, b) -> {
|
||||
Object ta = a.get("createdAt");
|
||||
Object tb = b.get("createdAt");
|
||||
if (ta == null && tb == null) return 0;
|
||||
if (ta == null) return 1;
|
||||
if (tb == null) return -1;
|
||||
return String.valueOf(tb).compareTo(String.valueOf(ta));
|
||||
});
|
||||
|
||||
return aliyunRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 DNS 解析记录(阿里云 + 本地审计)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> addRecord(Map<String, Object> body) {
|
||||
// 1. 调用阿里云 API 创建
|
||||
Map<String, Object> result = pythonApiClient.addDnsRecord(body);
|
||||
|
||||
// 2. 提取 record_id 并存本地审计
|
||||
Object dataObj = result.get("data");
|
||||
String recordId = null;
|
||||
if (dataObj instanceof Map) {
|
||||
recordId = String.valueOf(((Map<?, ?>) dataObj).get("record_id"));
|
||||
}
|
||||
|
||||
if (recordId != null && !recordId.isEmpty() && !"null".equals(recordId)) {
|
||||
DnsRecord localRecord = DnsRecord.builder()
|
||||
.recordId(recordId)
|
||||
.domainZone(String.valueOf(body.getOrDefault("zone", "")))
|
||||
.rr(String.valueOf(body.getOrDefault("rr", "")))
|
||||
.recordType(String.valueOf(body.getOrDefault("type", "A")))
|
||||
.recordValue(String.valueOf(body.getOrDefault("value", "")))
|
||||
.ttl(body.get("ttl") instanceof Integer ? (Integer) body.get("ttl") : 600)
|
||||
.status(1)
|
||||
.build();
|
||||
dnsRecordMapper.insert(localRecord);
|
||||
log.info("DNS 审计记录已保存: recordId={}, rr={}.{}", recordId, localRecord.getRr(), localRecord.getDomainZone());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 DNS 解析记录(阿里云 + 本地审计)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> updateRecord(String recordId, Map<String, Object> body) {
|
||||
// 1. 调用阿里云 API 更新
|
||||
Map<String, Object> result = pythonApiClient.updateDnsRecord(recordId, body);
|
||||
|
||||
// 2. 更新本地审计记录
|
||||
DnsRecord localRecord = dnsRecordMapper.selectOne(
|
||||
new LambdaQueryWrapper<DnsRecord>()
|
||||
.eq(DnsRecord::getRecordId, recordId)
|
||||
.eq(DnsRecord::getStatus, 1));
|
||||
|
||||
if (localRecord != null) {
|
||||
if (body.containsKey("rr")) {
|
||||
localRecord.setRr(String.valueOf(body.get("rr")));
|
||||
}
|
||||
if (body.containsKey("type")) {
|
||||
localRecord.setRecordType(String.valueOf(body.get("type")));
|
||||
}
|
||||
if (body.containsKey("value")) {
|
||||
localRecord.setRecordValue(String.valueOf(body.get("value")));
|
||||
}
|
||||
if (body.containsKey("ttl")) {
|
||||
localRecord.setTtl(body.get("ttl") instanceof Integer ? (Integer) body.get("ttl") : 600);
|
||||
}
|
||||
// updatedBy/updatedAt 由 MyMetaObjectHandler 自动填充
|
||||
dnsRecordMapper.updateById(localRecord);
|
||||
log.info("DNS 审计记录已更新: recordId={}", recordId);
|
||||
} else {
|
||||
// 本地无记录但有阿里云记录(可能是旧数据),尝试补建
|
||||
log.info("DNS 审计记录不存在,尝试补建: recordId={}", recordId);
|
||||
// 从 body 推断信息
|
||||
DnsRecord newAudit = DnsRecord.builder()
|
||||
.recordId(recordId)
|
||||
.domainZone(String.valueOf(body.getOrDefault("zone", "")))
|
||||
.rr(String.valueOf(body.getOrDefault("rr", "")))
|
||||
.recordType(String.valueOf(body.getOrDefault("type", "A")))
|
||||
.recordValue(String.valueOf(body.getOrDefault("value", "")))
|
||||
.ttl(body.get("ttl") instanceof Integer ? (Integer) body.get("ttl") : 600)
|
||||
.status(1)
|
||||
.build();
|
||||
dnsRecordMapper.insert(newAudit);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 DNS 解析记录(阿里云 + 本地软删除)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> deleteRecord(String recordId) {
|
||||
// 1. 调用阿里云 API 删除
|
||||
Map<String, Object> result = pythonApiClient.deleteDnsRecord(recordId);
|
||||
|
||||
// 2. 本地软删除
|
||||
DnsRecord localRecord = dnsRecordMapper.selectOne(
|
||||
new LambdaQueryWrapper<DnsRecord>()
|
||||
.eq(DnsRecord::getRecordId, recordId)
|
||||
.eq(DnsRecord::getStatus, 1));
|
||||
if (localRecord != null) {
|
||||
dnsRecordMapper.deleteById(localRecord.getId());
|
||||
log.info("DNS 审计记录已软删除: recordId={}", recordId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mokee.nginx.entity.ForwardingRule;
|
||||
import com.mokee.nginx.mapper.ForwardingRuleMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 转发规则业务逻辑层(只读模式)
|
||||
* 数据来源:宝塔面板 API
|
||||
* 新增/修改/删除操作请在宝塔面板上操作
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ForwardingRuleService {
|
||||
|
||||
@Autowired
|
||||
private ForwardingRuleMapper forwardingRuleMapper;
|
||||
|
||||
@Autowired
|
||||
private BTPanelApiClient btPanelApiClient;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 获取所有转发规则(从宝塔面板拉取)
|
||||
*
|
||||
* 流程:
|
||||
* 1. POST /data action=getData&table=sites → 获取站点列表
|
||||
* 2. 遍历站点:project_type="proxy" 的站点直接解析 project_config
|
||||
* 3. 非 proxy 站点调 POST /site action=GetProxyList 检查是否有 nginx 级反代
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<ForwardingRule> listRules() {
|
||||
List<ForwardingRule> rules = new ArrayList<>();
|
||||
long index = 1;
|
||||
|
||||
try {
|
||||
// 1. 获取站点列表
|
||||
Map<String, Object> siteResult = btPanelApiClient.getSiteList();
|
||||
List<Map<String, Object>> sites = siteResult != null
|
||||
? (List<Map<String, Object>>) siteResult.get("data")
|
||||
: null;
|
||||
|
||||
if (sites == null || sites.isEmpty()) {
|
||||
log.info("宝塔站点列表为空");
|
||||
return rules;
|
||||
}
|
||||
|
||||
log.info("宝塔站点数量: {}", sites.size());
|
||||
|
||||
// 2. 遍历每个站点
|
||||
for (Map<String, Object> site : sites) {
|
||||
String siteName = (String) site.get("name");
|
||||
if (siteName == null || siteName.isEmpty()) continue;
|
||||
|
||||
boolean isRunning = "1".equals(String.valueOf(site.getOrDefault("status", "1")));
|
||||
String projectType = (String) site.getOrDefault("project_type", "");
|
||||
|
||||
// 统一通过 GetProxyList 获取反代规则(适用于 proxy 类型和普通站点)
|
||||
List<Map<String, Object>> proxies = btPanelApiClient.getProxyList(siteName);
|
||||
|
||||
if (proxies != null && !proxies.isEmpty()) {
|
||||
for (Map<String, Object> proxy : proxies) {
|
||||
ForwardingRule rule = buildRuleFromProxyData(
|
||||
index++, siteName, site, proxy, isRunning);
|
||||
rules.add(rule);
|
||||
}
|
||||
} else if ("proxy".equals(projectType)) {
|
||||
// proxy 类型但 GetProxyList 为空:降级显示
|
||||
ForwardingRule rule = ForwardingRule.builder()
|
||||
.id(index++)
|
||||
.domain(siteName)
|
||||
.protocol("https")
|
||||
.target("[反代项目 - 点击查看详情]")
|
||||
.status(isRunning ? 1 : 0)
|
||||
.build();
|
||||
rules.add(rule);
|
||||
}
|
||||
// 非 proxy 且无反代规则的站点不显示
|
||||
}
|
||||
|
||||
log.info("从宝塔获取到 {} 条转发规则", rules.size());
|
||||
} catch (Exception e) {
|
||||
log.error("从宝塔获取转发规则失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取转发规则失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从宝塔"反向代理项目"中解析转发规则
|
||||
* project_config 包含: proxy_pass, domain_list, https_port 等
|
||||
*/
|
||||
private ForwardingRule buildRuleFromProxyProject(long id, String siteName,
|
||||
Map<String, Object> site, boolean isRunning) {
|
||||
try {
|
||||
String configJson = (String) site.get("project_config");
|
||||
if (configJson == null || configJson.isEmpty()) return null;
|
||||
|
||||
Map<String, Object> config = objectMapper.readValue(configJson, Map.class);
|
||||
|
||||
// 提取目标地址
|
||||
String proxyPass = (String) config.get("proxy_pass");
|
||||
if (proxyPass == null || proxyPass.isEmpty()) {
|
||||
// 可能存的是 proxy_dir 格式
|
||||
Object proxyDirObj = config.get("proxy_dir");
|
||||
if (proxyDirObj instanceof List && !((List<?>) proxyDirObj).isEmpty()) {
|
||||
Map<String, Object> firstProxy = (Map<String, Object>) ((List<?>) proxyDirObj).get(0);
|
||||
proxyPass = (String) firstProxy.get("proxy_pass");
|
||||
}
|
||||
}
|
||||
|
||||
String target = proxyPass != null ? proxyPass : "未知目标";
|
||||
|
||||
// 协议判断:有 https_port 配置则为 https
|
||||
String httpsPort = (String) config.get("https_port");
|
||||
String protocol = (httpsPort != null && !httpsPort.isEmpty()) ? "https" : "http";
|
||||
|
||||
return ForwardingRule.builder()
|
||||
.id(id)
|
||||
.domain(siteName)
|
||||
.protocol(protocol)
|
||||
.target(target)
|
||||
.status(isRunning ? 1 : 0)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.warn("解析 {} 的 project_config 失败: {}", siteName, e.getMessage());
|
||||
// 降级:仍然显示站点,target 标记为未知
|
||||
return ForwardingRule.builder()
|
||||
.id(id)
|
||||
.domain(siteName)
|
||||
.protocol("http")
|
||||
.target("解析失败,请查看宝塔面板")
|
||||
.status(isRunning ? 1 : 0)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 nginx 级反向代理数据中组装规则
|
||||
*/
|
||||
private ForwardingRule buildRuleFromProxyData(long id, String siteName,
|
||||
Map<String, Object> site, Map<String, Object> proxy, boolean isRunning) {
|
||||
|
||||
String proxysite = (String) proxy.getOrDefault("proxysite", "");
|
||||
String proxydir = (String) proxy.getOrDefault("proxydir", "/");
|
||||
|
||||
String protocol = proxysite.startsWith("https://") ? "https" : "http";
|
||||
String target = proxysite;
|
||||
if (!"/".equals(proxydir) && proxydir != null && !proxydir.isEmpty()) {
|
||||
target = proxysite + " (路径: " + proxydir + ")";
|
||||
}
|
||||
|
||||
return ForwardingRule.builder()
|
||||
.id(id)
|
||||
.domain(siteName)
|
||||
.protocol(protocol)
|
||||
.target(target)
|
||||
.status(isRunning ? 1 : 0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 以下写操作已废弃
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@Deprecated
|
||||
public ForwardingRule addRule(Object request) {
|
||||
throw new UnsupportedOperationException("新增转发规则请在宝塔面板上操作");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public ForwardingRule updateRule(Long id, Object request) {
|
||||
throw new UnsupportedOperationException("修改转发规则请在宝塔面板上操作");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void deleteRule(Long id) {
|
||||
throw new UnsupportedOperationException("删除转发规则请在宝塔面板上操作");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void reloadNginx() {
|
||||
throw new UnsupportedOperationException("重载Nginx请在宝塔面板上操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则详情:配置文件内容 + SSL证书信息
|
||||
*/
|
||||
public Map<String, Object> getConfig(Long id) {
|
||||
// 从列表中匹配域名
|
||||
List<ForwardingRule> rules = listRules();
|
||||
ForwardingRule rule = rules.stream()
|
||||
.filter(r -> r.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("规则不存在: id=" + id));
|
||||
|
||||
String domain = rule.getDomain();
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("domain", domain);
|
||||
result.put("protocol", rule.getProtocol());
|
||||
result.put("target", rule.getTarget());
|
||||
|
||||
// 获取 nginx 配置文件内容
|
||||
try {
|
||||
String configContent = btPanelApiClient.getNginxConfig(domain);
|
||||
result.put("configContent", configContent != null ? configContent : "");
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 {} 配置文件失败: {}", domain, e.getMessage());
|
||||
result.put("configContent", "");
|
||||
}
|
||||
|
||||
// 获取 SSL 证书信息
|
||||
try {
|
||||
List<Map<String, Object>> orders = btPanelApiClient.getCertOrders();
|
||||
Map<String, Object> matchedCert = null;
|
||||
if (orders != null) {
|
||||
for (Map<String, Object> order : orders) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> domains = (List<String>) order.get("domains");
|
||||
if (domains != null && domains.contains(domain)) {
|
||||
matchedCert = order;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchedCert != null) {
|
||||
result.put("sslStatus", String.valueOf(matchedCert.getOrDefault("status", "unknown")));
|
||||
Object expiresObj = matchedCert.get("cert_timeout");
|
||||
if (expiresObj == null) expiresObj = matchedCert.get("expires");
|
||||
if (expiresObj instanceof Number) {
|
||||
long expiresTs = ((Number) expiresObj).longValue();
|
||||
java.time.LocalDateTime expireDate = java.time.LocalDateTime.ofInstant(
|
||||
java.time.Instant.ofEpochSecond(expiresTs),
|
||||
java.time.ZoneId.of("Asia/Shanghai"));
|
||||
result.put("sslExpireDate", expireDate.toString());
|
||||
long daysLeft = java.time.Duration.between(
|
||||
java.time.LocalDateTime.now(), expireDate).toDays();
|
||||
result.put("sslDaysLeft", daysLeft);
|
||||
}
|
||||
result.put("sslAuthType", String.valueOf(matchedCert.getOrDefault("auth_type", "")));
|
||||
} else {
|
||||
result.put("sslStatus", "none");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 {} SSL信息失败: {}", domain, e.getMessage());
|
||||
result.put("sslStatus", "error");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> updateConfig(Long id, String content) {
|
||||
throw new UnsupportedOperationException("配置文件编辑已不可用");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> getSslFiles(Long id) {
|
||||
throw new UnsupportedOperationException("SSL证书查看请前往宝塔面板");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> getGzipConfig(Long id) {
|
||||
throw new UnsupportedOperationException("内容压缩配置请前往宝塔面板查看");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> updateGzipConfig(Long id, Map<String, Object> gzipConfig) {
|
||||
throw new UnsupportedOperationException("内容压缩配置请前往宝塔面板修改");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> getDomainLogs(Long id, int lines) {
|
||||
throw new UnsupportedOperationException("日志查看请前往宝塔面板");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Map<String, Object> getNginxStatus() {
|
||||
throw new UnsupportedOperationException("Nginx状态请前往宝塔面板查看");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package com.mokee.nginx.service;
|
||||
|
||||
import com.mokee.nginx.dto.RuleRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Python API HTTP 客户端
|
||||
* 调用 10.1.1.160:5000 上的 Nginx 管理 API
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PythonApiClient {
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
/** 证书操作专用(签发/续期超时 5分钟) */
|
||||
@Autowired
|
||||
@org.springframework.beans.factory.annotation.Qualifier("certRestTemplate")
|
||||
private RestTemplate certRestTemplate;
|
||||
|
||||
/**
|
||||
* 获取所有Nginx转发规则
|
||||
*/
|
||||
public Map<String, Object> getRules() {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("调用Python API获取规则列表失败: {}", e.getMessage());
|
||||
throw new RuntimeException("无法连接到Nginx管理服务: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增转发规则(HTTPS 可能触发 LE 签发,用长超时 RestTemplate)
|
||||
*/
|
||||
public Map<String, Object> addRule(RuleRequest request) {
|
||||
try {
|
||||
Map<String, Object> body = Map.of(
|
||||
"domain", request.getDomain(),
|
||||
"protocol", request.getProtocol(),
|
||||
"target", request.getTarget()
|
||||
);
|
||||
// HTTPS + zgitm 域名会触发 LE 签发(40s+),用长超时
|
||||
RestTemplate rt = "https".equals(request.getProtocol()) ? certRestTemplate : restTemplate;
|
||||
ResponseEntity<Map<String, Object>> response = rt.exchange(
|
||||
"/api/nginx/rules",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("调用Python API新增规则失败: {}", e.getMessage());
|
||||
throw new RuntimeException("新增Nginx规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改转发规则(HTTPS 可能触发 LE 签发,用长超时 RestTemplate)
|
||||
*/
|
||||
public Map<String, Object> updateRule(String domain, RuleRequest request) {
|
||||
try {
|
||||
Map<String, Object> body = Map.of(
|
||||
"domain", request.getDomain(),
|
||||
"protocol", request.getProtocol(),
|
||||
"target", request.getTarget()
|
||||
);
|
||||
RestTemplate rt = "https".equals(request.getProtocol()) ? certRestTemplate : restTemplate;
|
||||
ResponseEntity<Map<String, Object>> response = rt.exchange(
|
||||
"/api/nginx/rules/{domain}",
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(body),
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("调用Python API修改规则失败: {}", e.getMessage());
|
||||
throw new RuntimeException("修改Nginx规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除转发规则
|
||||
*/
|
||||
public Map<String, Object> deleteRule(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}",
|
||||
HttpMethod.DELETE,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("调用Python API删除规则失败: {}", e.getMessage());
|
||||
throw new RuntimeException("删除Nginx规则失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载Nginx配置
|
||||
*/
|
||||
public Map<String, Object> reloadNginx() {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/reload",
|
||||
HttpMethod.POST,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("调用Python API重载Nginx失败: {}", e.getMessage());
|
||||
throw new RuntimeException("重载Nginx配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名的 Nginx 配置文件原始内容
|
||||
*/
|
||||
public Map<String, Object> getRuleConfig(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}/config",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取配置文件失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取配置文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接写入 Nginx 配置文件(会触发语法检查和重载,失败自动回滚)
|
||||
*/
|
||||
public Map<String, Object> updateRuleConfig(String domain, String content) {
|
||||
try {
|
||||
Map<String, Object> body = Map.of("content", content);
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}/config",
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(body),
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("更新配置文件失败: {}", e.getMessage());
|
||||
throw new RuntimeException("更新配置文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名的 SSL 证书和密钥文件内容
|
||||
*/
|
||||
public Map<String, Object> getSslFiles(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}/ssl",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取SSL文件失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取SSL文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 内容压缩(gzip)API
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 获取指定域名的 gzip 压缩配置
|
||||
*/
|
||||
public Map<String, Object> getGzipConfig(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}/gzip",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取gzip配置失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取gzip配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定域名的 gzip 压缩配置
|
||||
*/
|
||||
public Map<String, Object> updateGzipConfig(String domain, Map<String, Object> gzipConfig) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/rules/{domain}/gzip",
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(gzipConfig),
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("更新gzip配置失败: {}", e.getMessage());
|
||||
throw new RuntimeException("更新gzip配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名的 Nginx 日志
|
||||
*/
|
||||
public Map<String, Object> getDomainLogs(String domain, int lines) {
|
||||
try {
|
||||
String path = "/api/nginx/rules/" + domain + "/logs?lines=" + lines;
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
path,
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取域名日志失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取域名日志失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
public boolean healthCheck() {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/health",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
Map<String, Object> body = response.getBody();
|
||||
return body != null && Integer.valueOf(0).equals(body.get("code"));
|
||||
} catch (Exception e) {
|
||||
log.warn("Python API健康检查失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 证书管理 API(Let's Encrypt)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 获取单个域名证书信息
|
||||
*/
|
||||
public Map<String, Object> getCertInfo(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/certs/{domain}",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
Map<String, Object> body = response.getBody();
|
||||
return body != null ? (Map<String, Object>) body.get("data") : null;
|
||||
} catch (Exception e) {
|
||||
log.error("获取证书信息失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取证书信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签发 Let's Encrypt 证书(超时: 5分钟)
|
||||
*/
|
||||
public Map<String, Object> issueCert(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = certRestTemplate.exchange(
|
||||
"/api/nginx/certs/{domain}/issue",
|
||||
HttpMethod.POST,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("签发证书失败: {}", e.getMessage());
|
||||
throw new RuntimeException("签发证书失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期 Let's Encrypt 证书(超时: 5分钟)
|
||||
*/
|
||||
public Map<String, Object> renewCert(String domain) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = certRestTemplate.exchange(
|
||||
"/api/nginx/certs/{domain}/renew",
|
||||
HttpMethod.POST,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {},
|
||||
domain
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("续期证书失败: {}", e.getMessage());
|
||||
throw new RuntimeException("续期证书失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 LE 证书信息列表
|
||||
*/
|
||||
public Map<String, Object> listCerts() {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/certs",
|
||||
HttpMethod.GET,
|
||||
null,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取证书列表失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取证书列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DNS 解析管理 API
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 获取 DNS 记录列表
|
||||
*/
|
||||
public Map<String, Object> listDnsRecords(String zone) {
|
||||
try {
|
||||
String path = "/api/nginx/dns/records?zone=" + zone;
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
path, HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<>() {});
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("获取DNS记录失败: {}", e.getMessage());
|
||||
throw new RuntimeException("获取DNS记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 DNS 记录
|
||||
*/
|
||||
public Map<String, Object> addDnsRecord(Map<String, Object> body) {
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body);
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/dns/records", HttpMethod.POST, request,
|
||||
new ParameterizedTypeReference<>() {});
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("添加DNS记录失败: {}", e.getMessage());
|
||||
throw new RuntimeException("添加DNS记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 DNS 记录
|
||||
*/
|
||||
public Map<String, Object> deleteDnsRecord(String recordId) {
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/dns/records/{recordId}", HttpMethod.DELETE, null,
|
||||
new ParameterizedTypeReference<>() {}, recordId);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("删除DNS记录失败: {}", e.getMessage());
|
||||
throw new RuntimeException("删除DNS记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 DNS 记录
|
||||
*/
|
||||
public Map<String, Object> updateDnsRecord(String recordId, Map<String, Object> body) {
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body);
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
"/api/nginx/dns/records/{recordId}", HttpMethod.PUT, request,
|
||||
new ParameterizedTypeReference<>() {}, recordId);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error("修改DNS记录失败: {}", e.getMessage());
|
||||
throw new RuntimeException("修改DNS记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# ==========================================
|
||||
# PROD 生产环境配置
|
||||
# 激活方式: mvn spring-boot:run -Pprod
|
||||
# 或 java -jar xxx.jar --spring.profiles.active=prod
|
||||
# ==========================================
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://10.1.1.122:3306/nginxserver?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: nginxserver
|
||||
password: mokee.
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.mokee.nginx: INFO
|
||||
org.springframework.web: WARN
|
||||
15
nginx-manager-backend/src/main/resources/application-qa.yml
Normal file
15
nginx-manager-backend/src/main/resources/application-qa.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# ==========================================
|
||||
# QA 测试环境配置
|
||||
# 激活方式: mvn spring-boot:run -Pqa
|
||||
# 或 java -jar xxx.jar --spring.profiles.active=qa
|
||||
# ==========================================
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://10.1.1.122:3306/nginxserver?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: nginxserver
|
||||
password: mokee.
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.mokee.nginx: DEBUG
|
||||
47
nginx-manager-backend/src/main/resources/application.yml
Normal file
47
nginx-manager-backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
# ==========================================
|
||||
# 基础配置(所有环境共享)
|
||||
# ==========================================
|
||||
server:
|
||||
port: 32402
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: nginx-manager
|
||||
|
||||
# 由 pom.xml Maven profile 控制,编译时替换:qa 或 prod
|
||||
profiles:
|
||||
active: @spring.profiles.active@
|
||||
|
||||
# Python Nginx API 连接配置(所有环境指向同一台 Nginx 服务器)
|
||||
python-api:
|
||||
base-url: http://101.132.183.138:32210
|
||||
connect-timeout: 5000
|
||||
read-timeout: 30000
|
||||
cert-read-timeout: 300000
|
||||
|
||||
# 宝塔面板 API 配置(只读:仅获取反代列表和证书列表)
|
||||
bt-panel:
|
||||
base-url: https://101.132.183.138:18888
|
||||
api-key: ahv5RRcYm3Vi2PzEFvqYsZvsS8LeGkYp
|
||||
|
||||
# Mokee Gateway 对外开放 API 配置(证书邮件通知用)
|
||||
gateway:
|
||||
base-url: https://gw.server.zgitm.com
|
||||
system-code: nginxServer
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
type-aliases-package: com.mokee.nginx.entity
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.mokee.nginx: DEBUG
|
||||
org.springframework.web: INFO
|
||||
Reference in New Issue
Block a user