info
This commit is contained in:
80
pom.xml
Normal file
80
pom.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ydoyun</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.6.4</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web (RestController/接口) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot JDBC -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<!-- SQL Server Driver -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
|
||||
<!-- <artifactId>mssql-jdbc</artifactId>-->
|
||||
<!-- <version>8.4.1.jre8</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
|
||||
<!-- <artifactId>mssql-jdbc</artifactId>-->
|
||||
<!-- <version>6.4.0.jre8</version>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid</artifactId>
|
||||
<version>1.2.15</version> <!-- 可以用最新版 -->
|
||||
</dependency>
|
||||
|
||||
<!-- SQL Server 2008 驱动(6.4.0 对应 SQLServer 2008 + JDK8) -->
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<version>6.4.0.jre8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- Bean Validation,用于参数校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Spring Boot Maven Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
22
src/main/java/com/ydoyun/mssqljdbc/MssqlJdbcApplication.java
Normal file
22
src/main/java/com/ydoyun/mssqljdbc/MssqlJdbcApplication.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.ydoyun.mssqljdbc;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 应用启动入口类。
|
||||
* <p>
|
||||
* 负责启动 Spring Boot 应用。
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class MssqlJdbcApplication {
|
||||
|
||||
/**
|
||||
* 启动 Spring Boot 应用。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MssqlJdbcApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ydoyun.mssqljdbc.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ApiResponseVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 基于 API Key 的简单请求头鉴权过滤器。
|
||||
* <p>
|
||||
* 统一拦截以 /api/ 开头的接口,校验请求头中的 X-API-KEY 是否与配置一致,
|
||||
* 如果不一致则直接返回未授权的 JSON 响应。
|
||||
*/
|
||||
@Component
|
||||
public class ApiKeyAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String HEADER_NAME = "X-API-KEY";
|
||||
|
||||
@Value("${security.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String requestUri = request.getRequestURI();
|
||||
|
||||
// 放行非业务接口(例如静态资源、健康检查等),这里只限制 /api/ 开头的请求
|
||||
if (!requestUri.startsWith("/api/")) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预检请求直接放行,避免影响跨域
|
||||
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String requestKey = request.getHeader(HEADER_NAME);
|
||||
if (requestKey == null || !requestKey.equals(apiKey)) {
|
||||
// 统一返回未授权的 JSON 结构
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
ApiResponseVO body = ApiResponseVO.unauthorized("API Key 不正确");
|
||||
response.getWriter().write(objectMapper.writeValueAsString(body));
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.ydoyun.mssqljdbc.controller;
|
||||
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ApiResponseVO;
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ProcedureRequestVO;
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ReportSyncRequestVO;
|
||||
import com.ydoyun.mssqljdbc.controller.vo.SqlRequestVO;
|
||||
import com.ydoyun.mssqljdbc.service.ProcedureService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 存储过程和报表相关接口控制器。
|
||||
* <p>
|
||||
* 对外提供:
|
||||
* <ul>
|
||||
* <li>动态调用存储过程接口</li>
|
||||
* <li>通用 SQL 执行接口</li>
|
||||
* <li>报表数据同步接口</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* API Key 校验已经通过全局过滤器 {@code ApiKeyAuthFilter} 统一处理,
|
||||
* 此处无需再在每个方法中手动校验请求头。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/procedure")
|
||||
@Slf4j
|
||||
public class ProcedureController {
|
||||
|
||||
@Autowired
|
||||
private ProcedureService procedureService;
|
||||
|
||||
/**
|
||||
* 动态调用存储过程查询数据。
|
||||
*
|
||||
* @param request 请求体,包含要连接的数据库信息、存储过程名称和参数
|
||||
* @return 统一封装的接口返回结果
|
||||
*/
|
||||
@PostMapping("/execute")
|
||||
public ApiResponseVO executeProcedure(
|
||||
@RequestBody @Valid ProcedureRequestVO request
|
||||
) {
|
||||
log.info("收到存储过程调用请求,dbHost={}, dbName={}, procedureName={}",
|
||||
request.getReportDatabase().getHost(),
|
||||
request.getReportDatabase().getDatabaseName(),
|
||||
request.getProcedureName());
|
||||
try {
|
||||
return ApiResponseVO.success(procedureService.callProcedure(
|
||||
request.getReportDatabase(),
|
||||
request.getProcedureName(),
|
||||
request.getParams()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("执行存储过程出错", e);
|
||||
return ApiResponseVO.error("执行存储过程出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 SQL 执行接口。
|
||||
* <p>
|
||||
* 根据请求体中的 SQL 文本自动判断是查询还是更新,
|
||||
* 并在指定的目标数据库上执行。
|
||||
*
|
||||
* @param request 请求体,包含要连接的数据库信息和 SQL 语句
|
||||
* @return 查询时返回数据列表,更新时返回影响行数
|
||||
*/
|
||||
@PostMapping("/exec-sql")
|
||||
public ApiResponseVO executeSql(
|
||||
@RequestBody @Valid SqlRequestVO request
|
||||
) {
|
||||
log.info("收到 SQL 执行请求,dbHost={}, dbName={}",
|
||||
request.getReportDatabase().getHost(),
|
||||
request.getReportDatabase().getDatabaseName());
|
||||
try {
|
||||
Object result = procedureService.executeSql(request.getReportDatabase(), request.getSql());
|
||||
return ApiResponseVO.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("执行 SQL 出错", e);
|
||||
return ApiResponseVO.error("执行 SQL 出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报表数据同步接口。
|
||||
* <p>
|
||||
* 根据传入的报表 ID 以及多张报表相关表的数据,
|
||||
* 在目标报表数据库中先删除旧数据,再批量插入新数据,保持数据一致。
|
||||
*
|
||||
* @param request 请求体,包含报表 ID、目标报表数据库信息以及各表的数据列表
|
||||
* @return 同步结果描述
|
||||
*/
|
||||
@PostMapping("/sync")
|
||||
public ApiResponseVO syncReport(
|
||||
@RequestBody @Valid ReportSyncRequestVO request
|
||||
) {
|
||||
log.info("收到报表同步请求,dbHost={}, dbName={}, reportId={}",
|
||||
request.getReportDatabase().getHost(),
|
||||
request.getReportDatabase().getDatabaseName(),
|
||||
request.getReportId());
|
||||
try {
|
||||
Object result = procedureService.syncReportData(request);
|
||||
return ApiResponseVO.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("报表同步失败", e);
|
||||
return ApiResponseVO.error("报表同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一的接口返回对象。
|
||||
* <p>
|
||||
* 通过 code + msg + data 的形式对所有接口返回值进行封装:
|
||||
* <ul>
|
||||
* <li>code:0 表示成功,1 表示失败,401 表示未授权</li>
|
||||
* <li>msg:提示信息</li>
|
||||
* <li>data:具体业务数据</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiResponseVO {
|
||||
|
||||
/**
|
||||
* 状态码:0 = 成功, 1 = 失败, 401 = 未授权。
|
||||
*/
|
||||
private int code;
|
||||
|
||||
/**
|
||||
* 提示信息。
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 业务数据,对类型不作限制。
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 构造一个成功响应。
|
||||
*
|
||||
* @param data 业务数据
|
||||
* @return 封装后的响应对象
|
||||
*/
|
||||
public static ApiResponseVO success(Object data) {
|
||||
return new ApiResponseVO(0, "success", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个通用错误响应。
|
||||
*
|
||||
* @param msg 错误描述
|
||||
* @return 封装后的响应对象
|
||||
*/
|
||||
public static ApiResponseVO error(String msg) {
|
||||
return new ApiResponseVO(1, msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个未授权错误响应。
|
||||
*
|
||||
* @param msg 提示信息
|
||||
* @return 封装后的响应对象
|
||||
*/
|
||||
public static ApiResponseVO unauthorized(String msg) {
|
||||
return new ApiResponseVO(401, msg, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 存储过程调用请求对象。
|
||||
* <p>
|
||||
* 包含要连接的数据库信息、存储过程名称以及参数列表。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProcedureRequestVO {
|
||||
|
||||
/**
|
||||
* 需要连接的报表数据库信息。
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "报表数据库信息不能为空")
|
||||
private ReportDatabaseRespVO reportDatabase;
|
||||
|
||||
/**
|
||||
* 要调用的存储过程名称。
|
||||
*/
|
||||
@NotBlank(message = "存储过程名称不能为空")
|
||||
private String procedureName;
|
||||
|
||||
/**
|
||||
* 存储过程参数,Map 的遍历顺序即为参数的传入顺序。
|
||||
*/
|
||||
private LinkedHashMap<String, Object> params;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 报表数据库连接信息对象。
|
||||
* <p>
|
||||
* 用于描述一个可连接的报表数据库实例,包括类型、地址、端口、账号密码等。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReportDatabaseRespVO {
|
||||
|
||||
/** 主键 ID */
|
||||
private Long id;
|
||||
|
||||
/** 数据源名称 */
|
||||
private String dbName;
|
||||
|
||||
/** 数据库类型(mysql、pgsql、oracle、sqlserver) */
|
||||
@NotBlank(message = "数据库类型不能为空")
|
||||
private String dbType;
|
||||
|
||||
/** 数据库地址 */
|
||||
@NotBlank(message = "数据库地址不能为空")
|
||||
private String host;
|
||||
|
||||
/** 数据库端口(可选,未传则按默认端口处理) */
|
||||
private Integer port;
|
||||
|
||||
/** 数据库账号 */
|
||||
@NotBlank(message = "数据库账号不能为空")
|
||||
private String username;
|
||||
|
||||
/** 数据库密码(建议加密存储) */
|
||||
@NotBlank(message = "数据库密码不能为空")
|
||||
private String password;
|
||||
|
||||
/** 数据库名称 */
|
||||
@NotBlank(message = "数据库名称不能为空")
|
||||
private String databaseName;
|
||||
|
||||
/** 备注说明 */
|
||||
private String remark;
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ReportSyncRequestVO {
|
||||
private Long reportId;
|
||||
|
||||
private ReportDatabaseRespVO reportDatabase;
|
||||
private List<SyncReportVO> ydoyunReport;
|
||||
private List<SyncReportTemplateVO> ydoyunReportTemplate;
|
||||
private List<SyncReportTemplateTitleVO> ydoyunReportTemplateTitle;
|
||||
private List<SyncReportTemplateListVO> ydoyunReportTemplateList;
|
||||
private List<SyncTemplateThresholdVO> ydoyunReportTemplateThreshold;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 通用 SQL 执行请求对象。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SqlRequestVO {
|
||||
|
||||
/**
|
||||
* 需要连接的报表数据库信息。
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "报表数据库信息不能为空")
|
||||
private ReportDatabaseRespVO reportDatabase;
|
||||
|
||||
/**
|
||||
* 要执行的 SQL 语句。
|
||||
*/
|
||||
@NotBlank(message = "SQL 语句不能为空")
|
||||
private String sql;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 报表模板子表(存储数据内容)同步 VO。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncReportTemplateListVO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 报表id
|
||||
*/
|
||||
private Long reportId;
|
||||
/**
|
||||
* 关联主表ID
|
||||
*/
|
||||
private Long templateId;
|
||||
/**
|
||||
* 字段名称
|
||||
*/
|
||||
private String fieldName;
|
||||
/**
|
||||
* 预留字段1
|
||||
*/
|
||||
private String reserveField1;
|
||||
/**
|
||||
* 预留字段2
|
||||
*/
|
||||
private String reserveField2;
|
||||
/**
|
||||
* 预留字段3
|
||||
*/
|
||||
private String reserveField3;
|
||||
/**
|
||||
* 预留字段4
|
||||
*/
|
||||
private String reserveField4;
|
||||
/**
|
||||
* 预留字段5
|
||||
*/
|
||||
private String reserveField5;
|
||||
/**
|
||||
* 预留字段6
|
||||
*/
|
||||
private String reserveField6;
|
||||
/**
|
||||
* 预留字段7
|
||||
*/
|
||||
private String reserveField7;
|
||||
/**
|
||||
* 预留字段8
|
||||
*/
|
||||
private String reserveField8;
|
||||
/**
|
||||
* 预留字段9
|
||||
*/
|
||||
private String reserveField9;
|
||||
/**
|
||||
* 预留字段10
|
||||
*/
|
||||
private String reserveField10;
|
||||
/**
|
||||
* 预留字段11
|
||||
*/
|
||||
private String reserveField11;
|
||||
/**
|
||||
* 预留字段12
|
||||
*/
|
||||
private String reserveField12;
|
||||
/**
|
||||
* 预留字段13
|
||||
*/
|
||||
private String reserveField13;
|
||||
/**
|
||||
* 预留字段14
|
||||
*/
|
||||
private String reserveField14;
|
||||
/**
|
||||
* 预留字段15
|
||||
*/
|
||||
private String reserveField15;
|
||||
/**
|
||||
* 预留字段16
|
||||
*/
|
||||
private String reserveField16;
|
||||
/**
|
||||
* 预留字段17
|
||||
*/
|
||||
private String reserveField17;
|
||||
/**
|
||||
* 预留字段18
|
||||
*/
|
||||
private String reserveField18;
|
||||
/**
|
||||
* 预留字段19
|
||||
*/
|
||||
private String reserveField19;
|
||||
/**
|
||||
* 预留字段20
|
||||
*/
|
||||
private String reserveField20;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 报表模板子表(存储表头定义)同步 VO。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncReportTemplateTitleVO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 报表id
|
||||
*/
|
||||
private Long reportId;
|
||||
/**
|
||||
* 关联主表ID
|
||||
*/
|
||||
private Long templateId;
|
||||
/**
|
||||
* 预留字段1
|
||||
*/
|
||||
private String reserveField1;
|
||||
/**
|
||||
* 预留字段2
|
||||
*/
|
||||
private String reserveField2;
|
||||
/**
|
||||
* 预留字段3
|
||||
*/
|
||||
private String reserveField3;
|
||||
/**
|
||||
* 预留字段4
|
||||
*/
|
||||
private String reserveField4;
|
||||
/**
|
||||
* 预留字段5
|
||||
*/
|
||||
private String reserveField5;
|
||||
/**
|
||||
* 预留字段6
|
||||
*/
|
||||
private String reserveField6;
|
||||
/**
|
||||
* 预留字段7
|
||||
*/
|
||||
private String reserveField7;
|
||||
/**
|
||||
* 预留字段8
|
||||
*/
|
||||
private String reserveField8;
|
||||
/**
|
||||
* 预留字段9
|
||||
*/
|
||||
private String reserveField9;
|
||||
/**
|
||||
* 预留字段10
|
||||
*/
|
||||
private String reserveField10;
|
||||
/**
|
||||
* 预留字段11
|
||||
*/
|
||||
private String reserveField11;
|
||||
/**
|
||||
* 预留字段12
|
||||
*/
|
||||
private String reserveField12;
|
||||
/**
|
||||
* 预留字段13
|
||||
*/
|
||||
private String reserveField13;
|
||||
/**
|
||||
* 预留字段14
|
||||
*/
|
||||
private String reserveField14;
|
||||
/**
|
||||
* 预留字段15
|
||||
*/
|
||||
private String reserveField15;
|
||||
/**
|
||||
* 预留字段16
|
||||
*/
|
||||
private String reserveField16;
|
||||
/**
|
||||
* 预留字段17
|
||||
*/
|
||||
private String reserveField17;
|
||||
/**
|
||||
* 预留字段18
|
||||
*/
|
||||
private String reserveField18;
|
||||
/**
|
||||
* 预留字段19
|
||||
*/
|
||||
private String reserveField19;
|
||||
/**
|
||||
* 预留字段20
|
||||
*/
|
||||
private String reserveField20;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 报表规则模板同步 VO。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncReportTemplateVO {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 报表id
|
||||
*/
|
||||
private Long reportId;
|
||||
|
||||
/**
|
||||
* 模块名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 存储过程名称
|
||||
*/
|
||||
private String procedureName;
|
||||
|
||||
/**
|
||||
* 负责人
|
||||
*/
|
||||
private String owner;
|
||||
|
||||
/**
|
||||
* 复核人
|
||||
*/
|
||||
private String reviewer;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 报表配置同步 VO(仅作为数据承载对象,不依赖持久层框架)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncReportVO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 报表名称
|
||||
*/
|
||||
private String reportName;
|
||||
/**
|
||||
* 报表编码(唯一标识)
|
||||
*/
|
||||
private String reportCode;
|
||||
/**
|
||||
* 报表访问地址
|
||||
*/
|
||||
private String reportUrl;
|
||||
/**
|
||||
* 地址类型(00外链接、10内连接路由)
|
||||
*/
|
||||
private String urlType;
|
||||
/**
|
||||
* 绑定的数据源ID(ydoyun_report_database.id)
|
||||
*/
|
||||
private Long databaseId;
|
||||
/**
|
||||
* 状态(00草稿、10发布)
|
||||
*/
|
||||
private String status;
|
||||
/**
|
||||
* 备注说明
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ydoyun.mssqljdbc.controller.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 报表阈值相关配置同步 VO。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SyncTemplateThresholdVO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 报表id
|
||||
*/
|
||||
private Long reportId;
|
||||
/**
|
||||
* 关联主表ID(ydoyun_report_template.id)
|
||||
*/
|
||||
private Long templateId;
|
||||
/**
|
||||
* 关联主表ID(ydoyun_report_template_list.id)
|
||||
*/
|
||||
private Long templateListId;
|
||||
/**
|
||||
* 最小阈值
|
||||
*/
|
||||
private BigDecimal minThreshold;
|
||||
/**
|
||||
* 最大阈值
|
||||
*/
|
||||
private BigDecimal maxThreshold;
|
||||
/**
|
||||
* 阈值定义
|
||||
*/
|
||||
private String thresholdDef;
|
||||
/**
|
||||
* 话术
|
||||
*/
|
||||
private String script;
|
||||
/**
|
||||
* 建议
|
||||
*/
|
||||
private String suggestion;
|
||||
|
||||
|
||||
}
|
||||
560
src/main/java/com/ydoyun/mssqljdbc/service/ProcedureService.java
Normal file
560
src/main/java/com/ydoyun/mssqljdbc/service/ProcedureService.java
Normal file
@@ -0,0 +1,560 @@
|
||||
package com.ydoyun.mssqljdbc.service;
|
||||
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ReportDatabaseRespVO;
|
||||
import com.ydoyun.mssqljdbc.controller.vo.ReportSyncRequestVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.DefaultTransactionDefinition;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 存储过程调用与报表数据同步服务。
|
||||
* <p>
|
||||
* 核心职责:
|
||||
* <ul>
|
||||
* <li>根据请求动态创建数据源并执行存储过程/SQL</li>
|
||||
* <li>根据驼峰字段自动转换为下划线字段并批量写入数据库</li>
|
||||
* <li>在同一事务中完成多张报表相关表的数据同步</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ProcedureService {
|
||||
|
||||
/**
|
||||
* 根据请求中的数据库信息动态创建 DataSource。
|
||||
*
|
||||
* @param reportDatabase 目标数据库连接信息
|
||||
* @return 对应的 DataSource 实例
|
||||
*/
|
||||
private DataSource createDataSource(ReportDatabaseRespVO reportDatabase) {
|
||||
if (reportDatabase == null) {
|
||||
throw new IllegalArgumentException("reportDatabase 不能为空");
|
||||
}
|
||||
|
||||
String dbType = reportDatabase.getDbType();
|
||||
String driverClassName;
|
||||
String url;
|
||||
|
||||
// 目前主要支持 SQLServer,其他类型可以根据需要扩展
|
||||
if (dbType == null || dbType.isEmpty() || "sqlserver".equalsIgnoreCase(dbType)) {
|
||||
driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
|
||||
Integer port = reportDatabase.getPort();
|
||||
int portValue = (port != null) ? port : 1433;
|
||||
url = "jdbc:sqlserver://" + reportDatabase.getHost() + ":" + portValue
|
||||
+ ";databaseName=" + reportDatabase.getDatabaseName();
|
||||
} else {
|
||||
throw new IllegalArgumentException("暂不支持的数据库类型: " + dbType);
|
||||
}
|
||||
|
||||
DriverManagerDataSource dataSource = new DriverManagerDataSource();
|
||||
dataSource.setDriverClassName(driverClassName);
|
||||
dataSource.setUrl(url);
|
||||
dataSource.setUsername(reportDatabase.getUsername());
|
||||
dataSource.setPassword(reportDatabase.getPassword());
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据库信息创建对应的 JdbcTemplate。
|
||||
*
|
||||
* @param reportDatabase 目标数据库连接信息
|
||||
* @return 对应的 JdbcTemplate
|
||||
*/
|
||||
private JdbcTemplate createJdbcTemplate(ReportDatabaseRespVO reportDatabase) {
|
||||
return new JdbcTemplate(createDataSource(reportDatabase));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理结果集,将数据添加到结果列表中。
|
||||
*
|
||||
* @param rs 结果集
|
||||
* @param resultList 结果列表
|
||||
* @param resultSetIndex 结果集索引(用于日志)
|
||||
*/
|
||||
private void processResultSet(ResultSet rs, List<Map<String, Object>> resultList, int resultSetIndex) throws SQLException {
|
||||
if (rs == null) {
|
||||
log.warn("结果集 #{} 为 null", resultSetIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
ResultSetMetaData metaData = rs.getMetaData();
|
||||
int columnCount = metaData.getColumnCount();
|
||||
log.info("结果集 #{} 列数: {}", resultSetIndex, columnCount);
|
||||
|
||||
// 记录列名和类型
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
List<String> columnTypes = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
columnNames.add(metaData.getColumnLabel(i));
|
||||
columnTypes.add(metaData.getColumnTypeName(i));
|
||||
}
|
||||
log.info("结果集 #{} 列名: {}", resultSetIndex, columnNames);
|
||||
log.info("结果集 #{} 列类型: {}", resultSetIndex, columnTypes);
|
||||
|
||||
int rowCount = 0;
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
row.put(metaData.getColumnLabel(i), rs.getObject(i));
|
||||
}
|
||||
resultList.add(row);
|
||||
rowCount++;
|
||||
log.debug("结果集 #{} 第 {} 行数据: {}", resultSetIndex, rowCount, row);
|
||||
}
|
||||
|
||||
if (rowCount == 0) {
|
||||
log.info("结果集 #{} 为空(有 {} 列但无数据行)", resultSetIndex, columnCount);
|
||||
} else {
|
||||
log.info("结果集 #{} 读取了 {} 行数据", resultSetIndex, rowCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将参数值格式化为 SQL 字符串中的参数值。
|
||||
* 字符串类型会加单引号并转义单引号,其他类型直接转字符串。
|
||||
*
|
||||
* @param value 参数值
|
||||
* @return 格式化后的 SQL 参数值
|
||||
*/
|
||||
private String formatSqlParam(Object value) {
|
||||
if (value == null) {
|
||||
return "NULL";
|
||||
}
|
||||
if (value instanceof String) {
|
||||
// 转义单引号:' -> ''
|
||||
String escaped = ((String) value).replace("'", "''");
|
||||
return "'" + escaped + "'";
|
||||
}
|
||||
if (value instanceof Number || value instanceof Boolean) {
|
||||
return value.toString();
|
||||
}
|
||||
// 其他类型转为字符串并加单引号
|
||||
String str = value.toString().replace("'", "''");
|
||||
return "'" + str + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态调用存储过程,并返回结果集。
|
||||
* <p>
|
||||
* 使用 exec 语句直接执行存储过程,使用命名参数方式,例如:
|
||||
* exec YDY_AI_GET_SDXS @rq='2025-11-26',@ckdm='01',@p='123'
|
||||
* <p>
|
||||
* 参数 Map 的 key 作为参数名,value 作为参数值。
|
||||
* 如果参数名不以 @ 开头,会自动添加 @ 前缀。
|
||||
*
|
||||
* @param reportDatabase 目标数据库连接信息
|
||||
* @param procedureName 存储过程名称
|
||||
* @param params 存储过程的参数 Map(key 为参数名,value 为参数值)
|
||||
* @return 结果集列表
|
||||
*/
|
||||
public List<Map<String, Object>> callProcedure(ReportDatabaseRespVO reportDatabase,
|
||||
String procedureName,
|
||||
Map<String, Object> params) {
|
||||
log.info(
|
||||
"开始调用存储过程,dbHost={}, dbName={}, procedureName={}, params={}",
|
||||
reportDatabase.getHost(),
|
||||
reportDatabase.getDatabaseName(),
|
||||
procedureName,
|
||||
params
|
||||
);
|
||||
JdbcTemplate jdbcTemplate = createJdbcTemplate(reportDatabase);
|
||||
|
||||
// 构建 exec 语句:exec 存储过程名 @参数名1=值1,@参数名2=值2,...
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("exec ").append(procedureName);
|
||||
|
||||
if (params != null && !params.isEmpty()) {
|
||||
sql.append(" ");
|
||||
List<String> namedParams = new ArrayList<>();
|
||||
// 使用命名参数方式:@参数名=参数值
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
String paramName = entry.getKey();
|
||||
String formattedValue = formatSqlParam(entry.getValue());
|
||||
// 确保参数名以 @ 开头
|
||||
if (!paramName.startsWith("@")) {
|
||||
paramName = "@" + paramName;
|
||||
}
|
||||
namedParams.add(paramName + "=" + formattedValue);
|
||||
log.info("参数 [{}] = {} (格式化后: {}={})", entry.getKey(), entry.getValue(), paramName, formattedValue);
|
||||
}
|
||||
sql.append(String.join(",", namedParams));
|
||||
}
|
||||
|
||||
String finalSql = sql.toString();
|
||||
log.info("执行存储过程 SQL:{}", finalSql);
|
||||
|
||||
// 使用 Statement 执行 exec 语句,以便处理多个结果集
|
||||
List<Map<String, Object>> resultList = new ArrayList<>();
|
||||
jdbcTemplate.execute((Connection con) -> {
|
||||
try (Statement stmt = con.createStatement()) {
|
||||
// 尝试使用 executeQuery 直接获取结果集(会跳过前面的更新计数)
|
||||
// 如果失败,则使用 execute 方式
|
||||
try {
|
||||
log.info("尝试使用 executeQuery 执行存储过程");
|
||||
try (ResultSet rs = stmt.executeQuery(finalSql)) {
|
||||
processResultSet(rs, resultList, 0);
|
||||
}
|
||||
|
||||
// 继续获取后续结果集
|
||||
int resultSetIndex = 1;
|
||||
while (true) {
|
||||
boolean hasMore = stmt.getMoreResults();
|
||||
log.info("getMoreResults() 返回: {}", hasMore);
|
||||
if (!hasMore) {
|
||||
int updateCount = stmt.getUpdateCount();
|
||||
log.info("getMoreResults() 返回 false,updateCount: {}", updateCount);
|
||||
if (updateCount == -1) {
|
||||
// 检查是否真的有结果集
|
||||
ResultSet rs = stmt.getResultSet();
|
||||
if (rs != null) {
|
||||
// 确实有结果集,处理它
|
||||
try (ResultSet resultSet = rs) {
|
||||
processResultSet(resultSet, resultList, resultSetIndex);
|
||||
}
|
||||
resultSetIndex++;
|
||||
// 继续尝试获取下一个结果
|
||||
continue;
|
||||
} else {
|
||||
// updateCount 是 -1 但 getResultSet() 返回 null,说明没有更多结果了
|
||||
log.info("updateCount 为 -1 但 getResultSet() 返回 null,没有更多结果集了");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 真正没有更多结果了
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 有更多结果,处理它
|
||||
int updateCount = stmt.getUpdateCount();
|
||||
if (updateCount == -1) {
|
||||
try (ResultSet rs = stmt.getResultSet()) {
|
||||
if (rs != null) {
|
||||
processResultSet(rs, resultList, resultSetIndex);
|
||||
} else {
|
||||
log.warn("updateCount 为 -1 但 getResultSet() 返回 null");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("结果 #{} 是更新计数: {}", resultSetIndex, updateCount);
|
||||
}
|
||||
resultSetIndex++;
|
||||
}
|
||||
}
|
||||
log.info("使用 executeQuery 方式完成,共收集到 {} 行数据", resultList.size());
|
||||
} catch (SQLException e) {
|
||||
// executeQuery 失败,可能因为第一个结果是更新计数,改用 execute 方式
|
||||
log.info("executeQuery 失败,改用 execute 方式: {}", e.getMessage());
|
||||
// 执行存储过程
|
||||
boolean hasResultSet = stmt.execute(finalSql);
|
||||
log.info("存储过程执行完成,execute() 返回: {}", hasResultSet);
|
||||
|
||||
int resultSetIndex = 0;
|
||||
boolean continueProcessing = true;
|
||||
|
||||
// 处理所有结果集(包括空结果集)
|
||||
// 即使第一个结果是更新计数,也要继续获取后续的结果集
|
||||
while (continueProcessing) {
|
||||
int updateCount = stmt.getUpdateCount();
|
||||
log.info("处理结果 #{},updateCount: {}", resultSetIndex, updateCount);
|
||||
|
||||
// updateCount == -1 表示当前是结果集(ResultSet)
|
||||
// updateCount >= 0 表示是更新计数(DML 语句的影响行数)
|
||||
if (updateCount == -1) {
|
||||
// 处理结果集(即使为空也要处理)
|
||||
try (ResultSet rs = stmt.getResultSet()) {
|
||||
processResultSet(rs, resultList, resultSetIndex);
|
||||
} catch (Exception ex) {
|
||||
log.error("处理结果集 #{} 时出错", resultSetIndex, ex);
|
||||
throw ex;
|
||||
}
|
||||
} else {
|
||||
// 更新计数,记录日志但不收集数据
|
||||
log.info("结果 #{} 是更新计数: {}", resultSetIndex, updateCount);
|
||||
}
|
||||
|
||||
resultSetIndex++;
|
||||
|
||||
// 获取下一个结果
|
||||
// getMoreResults() 返回 true 表示还有更多结果,false 表示没有更多结果
|
||||
boolean hasMoreResults = stmt.getMoreResults();
|
||||
log.info("getMoreResults() 返回: {}", hasMoreResults);
|
||||
|
||||
// 检查是否还有更多结果
|
||||
if (!hasMoreResults) {
|
||||
int nextUpdateCount = stmt.getUpdateCount();
|
||||
log.info("getMoreResults() 返回 false,检查 updateCount: {}", nextUpdateCount);
|
||||
// 如果 updateCount 是 -1,检查是否真的有结果集
|
||||
if (nextUpdateCount == -1) {
|
||||
ResultSet nextRs = stmt.getResultSet();
|
||||
if (nextRs != null) {
|
||||
log.info("虽然 getMoreResults() 返回 false,但确实有结果集,继续处理");
|
||||
continueProcessing = true;
|
||||
} else {
|
||||
log.info("updateCount 为 -1 但 getResultSet() 返回 null,没有更多结果了");
|
||||
continueProcessing = false;
|
||||
}
|
||||
} else {
|
||||
// 真正没有更多结果了
|
||||
continueProcessing = false;
|
||||
}
|
||||
} else {
|
||||
continueProcessing = true;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("存储过程执行完成,共收集到 {} 行数据(来自 {} 个结果集)", resultList.size(), resultSetIndex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任意 SQL。
|
||||
* <p>
|
||||
* 自动根据 SQL 前缀是否为 SELECT 判断是查询还是更新,
|
||||
* 并在指定数据库上执行。
|
||||
*
|
||||
* @param reportDatabase 目标数据库信息
|
||||
* @param sql 任意 SQL 语句
|
||||
* @return 如果是查询 SQL,返回 List<Map<String,Object>>;如果是 DML(INSERT/UPDATE/DELETE),返回影响行数
|
||||
*/
|
||||
public Object executeSql(ReportDatabaseRespVO reportDatabase, String sql) {
|
||||
log.info("开始执行 SQL,dbHost={}, dbName={}, sql={}",
|
||||
reportDatabase.getHost(), reportDatabase.getDatabaseName(), sql);
|
||||
JdbcTemplate jdbcTemplate = createJdbcTemplate(reportDatabase);
|
||||
String lowerSql = sql.trim().toLowerCase();
|
||||
if (lowerSql.startsWith("select")) {
|
||||
// 查询 SQL
|
||||
return jdbcTemplate.queryForList(sql);
|
||||
} else {
|
||||
// 非查询 SQL
|
||||
return jdbcTemplate.update(sql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =====================================================================
|
||||
// ====================== ⭐ 新增:报表同步功能 ⭐ ======================
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* 将驼峰命名的字段名转换为下划线命名。
|
||||
*
|
||||
* @param str 驼峰格式字段名,例如 userName
|
||||
* @return 下划线格式字段名,例如 user_name
|
||||
*/
|
||||
private String camelToUnderline(String str) {
|
||||
if (str == null) return null;
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : str.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) {
|
||||
result.append("_").append(Character.toLowerCase(c));
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 把单条 Map 中的所有驼峰字段名转换成下划线字段名。
|
||||
*
|
||||
* @param data 原始数据 Map(key 为驼峰字段名)
|
||||
* @return key 已转换为下划线的 Map
|
||||
*/
|
||||
private Map<String, Object> convertCamelMap(Map<String, Object> data) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
data.forEach((key, value) -> {
|
||||
result.put(camelToUnderline(key), value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意 Java Bean 转换为 Map,key 为属性名,value 为属性值。
|
||||
* <p>
|
||||
* 会向上遍历父类(例如 BaseDO),并忽略 static 字段。
|
||||
*
|
||||
* @param bean Java Bean 对象
|
||||
* @return 字段名到字段值的映射
|
||||
*/
|
||||
private Map<String, Object> beanToMap(Object bean) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (bean == null) {
|
||||
return result;
|
||||
}
|
||||
Class<?> clazz = bean.getClass();
|
||||
while (clazz != null && clazz != Object.class) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
// 跳过静态字段
|
||||
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
result.put(field.getName(), field.get(bean));
|
||||
} catch (IllegalAccessException e) {
|
||||
log.warn("读取属性失败,class={}, field={}", clazz.getName(), field.getName(), e);
|
||||
}
|
||||
}
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量向指定表插入数据。
|
||||
* <p>
|
||||
* 会先将字段名从驼峰转换为下划线,并排除指定字段,
|
||||
* 然后组装 INSERT 语句并逐条执行。
|
||||
*
|
||||
* @param jdbcTemplate 对应数据源的 JdbcTemplate
|
||||
* @param table 目标表名
|
||||
* @param dataList 要插入的数据列表(可以是 Map 或 VO 对象)
|
||||
* @param excludeColumns 需要排除的字段名列表(下划线格式)
|
||||
*/
|
||||
private void batchInsert(JdbcTemplate jdbcTemplate, String table, List<?> dataList, List<String> excludeColumns) {
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Object item : dataList) {
|
||||
|
||||
// 支持原有 Map 结构,也支持新的 VO 对象
|
||||
Map<String, Object> source;
|
||||
if (item instanceof Map) {
|
||||
//noinspection unchecked
|
||||
source = (Map<String, Object>) item;
|
||||
} else {
|
||||
source = beanToMap(item);
|
||||
}
|
||||
|
||||
// 转下划线
|
||||
Map<String, Object> row = convertCamelMap(source);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
StringBuilder values = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
row.forEach((k, v) -> {
|
||||
// 排除指定列
|
||||
if (excludeColumns != null && excludeColumns.contains(k)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fields.append(k).append(",");
|
||||
values.append("?,");
|
||||
|
||||
// 类型规范化
|
||||
if (v == null) {
|
||||
params.add(null);
|
||||
} else if (v instanceof String || v instanceof Number || v instanceof Boolean || v instanceof java.util.Date) {
|
||||
if (v instanceof java.util.Date) {
|
||||
params.add(new java.sql.Timestamp(((java.util.Date) v).getTime()));
|
||||
} else {
|
||||
params.add(v);
|
||||
}
|
||||
} else {
|
||||
params.add(v.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有字段,跳过
|
||||
if (fields.length() == 0) continue;
|
||||
|
||||
fields.deleteCharAt(fields.length() - 1);
|
||||
values.deleteCharAt(values.length() - 1);
|
||||
|
||||
String sql = "INSERT INTO " + table +
|
||||
" (" + fields + ") VALUES (" + values + ")";
|
||||
|
||||
jdbcTemplate.update(sql, params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ====================== 需要的同步方法 ======================
|
||||
|
||||
/**
|
||||
* 同步报表相关表的数据。
|
||||
* <p>
|
||||
* 基于传入的 {@link ReportSyncRequestVO}:
|
||||
* <ol>
|
||||
* <li>校验报表 ID 以及目标数据库信息</li>
|
||||
* <li>在同一事务中删除旧数据</li>
|
||||
* <li>批量插入新的报表配置和模板数据</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param request 报表同步请求对象,包含报表 ID、目标库信息及各表数据
|
||||
* @return 同步结果描述
|
||||
*/
|
||||
public String syncReportData(ReportSyncRequestVO request) {
|
||||
|
||||
ReportDatabaseRespVO reportDatabase = request.getReportDatabase();
|
||||
Long reportId = request.getReportId();
|
||||
if (reportId == null) {
|
||||
throw new RuntimeException("reportId 不能为空");
|
||||
}
|
||||
if (reportDatabase == null) {
|
||||
throw new RuntimeException("reportDatabase 不能为空");
|
||||
}
|
||||
|
||||
log.info("开始同步报表数据,dbHost={}, dbName={}, reportId={}",
|
||||
reportDatabase.getHost(), reportDatabase.getDatabaseName(), reportId);
|
||||
|
||||
// 为本次同步请求创建专用数据源与事务管理器,保证多表操作在同一事务中完成
|
||||
DataSource dataSource = createDataSource(reportDatabase);
|
||||
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
|
||||
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
|
||||
TransactionStatus status = transactionManager.getTransaction(def);
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
|
||||
try {
|
||||
// 删除旧数据
|
||||
jdbcTemplate.update("DELETE FROM ydoyun_report WHERE id=?", reportId);
|
||||
jdbcTemplate.update("DELETE FROM ydoyun_report_template WHERE report_id=?", reportId);
|
||||
jdbcTemplate.update("DELETE FROM ydoyun_report_template_title WHERE report_id=?", reportId);
|
||||
jdbcTemplate.update("DELETE FROM ydoyun_report_template_list WHERE report_id=?", reportId);
|
||||
jdbcTemplate.update("DELETE FROM ydoyun_report_template_threshold WHERE report_id=?", reportId);
|
||||
|
||||
// 新增数据,非空判断和 list size 判断
|
||||
if (request.getYdoyunReport() != null && !request.getYdoyunReport().isEmpty()) {
|
||||
batchInsert(jdbcTemplate, "ydoyun_report", request.getYdoyunReport(), Arrays.asList("trans_map"));
|
||||
}
|
||||
if (request.getYdoyunReportTemplate() != null && !request.getYdoyunReportTemplate().isEmpty()) {
|
||||
batchInsert(jdbcTemplate, "ydoyun_report_template", request.getYdoyunReportTemplate(), Arrays.asList("trans_map"));
|
||||
}
|
||||
if (request.getYdoyunReportTemplateTitle() != null && !request.getYdoyunReportTemplateTitle().isEmpty()) {
|
||||
batchInsert(jdbcTemplate, "ydoyun_report_template_title", request.getYdoyunReportTemplateTitle(), Arrays.asList("trans_map"));
|
||||
}
|
||||
if (request.getYdoyunReportTemplateList() != null && !request.getYdoyunReportTemplateList().isEmpty()) {
|
||||
batchInsert(jdbcTemplate, "ydoyun_report_template_list", request.getYdoyunReportTemplateList(), Arrays.asList("trans_map"));
|
||||
}
|
||||
if (request.getYdoyunReportTemplateThreshold() != null && !request.getYdoyunReportTemplateThreshold().isEmpty()) {
|
||||
batchInsert(jdbcTemplate, "ydoyun_report_template_threshold", request.getYdoyunReportTemplateThreshold(), Arrays.asList("trans_map"));
|
||||
}
|
||||
|
||||
transactionManager.commit(status);
|
||||
return "同步成功";
|
||||
} catch (Exception e) {
|
||||
transactionManager.rollback(status);
|
||||
log.error("同步报表数据失败,reportId={}, dbHost={}, dbName={}",
|
||||
reportId, reportDatabase.getHost(), reportDatabase.getDatabaseName(), e);
|
||||
throw new RuntimeException("同步报表数据失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
15
src/main/resources/application.yml
Normal file
15
src/main/resources/application.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
server:
|
||||
port: 49090
|
||||
address: 0.0.0.0 # 允许外部访问
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# SQL Server 2008 一般不强制使用加密,这里不主动开启 SSL,避免 TLS 协议不兼容问题
|
||||
url: jdbc:sqlserver://106.15.62.63:9943;databaseName=ERPLJSM
|
||||
username: bsai
|
||||
password: ljsm@bsAI
|
||||
|
||||
security:
|
||||
api-key: your_secret_api_key_123456
|
||||
15
target/classes/application.yml
Normal file
15
target/classes/application.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
server:
|
||||
port: 49090
|
||||
address: 0.0.0.0 # 允许外部访问
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# SQL Server 2008 一般不强制使用加密,这里不主动开启 SSL,避免 TLS 协议不兼容问题
|
||||
url: jdbc:sqlserver://106.15.62.63:9943;databaseName=ERPLJSM
|
||||
username: bsai
|
||||
password: ljsm@bsAI
|
||||
|
||||
security:
|
||||
api-key: your_secret_api_key_123456
|
||||
BIN
target/classes/com/ydoyun/mssqljdbc/MssqlJdbcApplication.class
Normal file
BIN
target/classes/com/ydoyun/mssqljdbc/MssqlJdbcApplication.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=mssql-jdbc
|
||||
groupId=com.ydoyun
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,14 @@
|
||||
com/ydoyun/mssqljdbc/controller/vo/SyncTemplateThresholdVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/ProcedureRequestVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/SyncReportVO.class
|
||||
com/ydoyun/mssqljdbc/MssqlJdbcApplication.class
|
||||
com/ydoyun/mssqljdbc/config/ApiKeyAuthFilter.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateListVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/ReportSyncRequestVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateTitleVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/ReportDatabaseRespVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/SqlRequestVO.class
|
||||
com/ydoyun/mssqljdbc/controller/vo/ApiResponseVO.class
|
||||
com/ydoyun/mssqljdbc/controller/ProcedureController.class
|
||||
com/ydoyun/mssqljdbc/service/ProcedureService.class
|
||||
@@ -0,0 +1,14 @@
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/ApiResponseVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateListVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/ReportDatabaseRespVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/MssqlJdbcApplication.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SyncTemplateThresholdVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/ReportSyncRequestVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/ProcedureController.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateTitleVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SyncReportTemplateVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/ProcedureRequestVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SyncReportVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/controller/vo/SqlRequestVO.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/service/ProcedureService.java
|
||||
/Users/ouhaolan/project/百盛科技/报表/mssql-jdbc/src/main/java/com/ydoyun/mssqljdbc/config/ApiKeyAuthFilter.java
|
||||
BIN
target/mssql-jdbc-1.0.0.jar
Normal file
BIN
target/mssql-jdbc-1.0.0.jar
Normal file
Binary file not shown.
BIN
target/mssql-jdbc-1.0.0.jar.original
Normal file
BIN
target/mssql-jdbc-1.0.0.jar.original
Normal file
Binary file not shown.
Reference in New Issue
Block a user