This commit is contained in:
2026-02-27 09:41:10 +08:00
commit 0a66cde347
37 changed files with 1493 additions and 0 deletions

0
README.md Normal file
View File

80
pom.xml Normal file
View 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>

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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>code0 表示成功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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 绑定的数据源IDydoyun_report_database.id
*/
private Long databaseId;
/**
* 状态(00草稿、10发布)
*/
private String status;
/**
* 备注说明
*/
private String remark;
}

View File

@@ -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;
/**
* 关联主表IDydoyun_report_template.id
*/
private Long templateId;
/**
* 关联主表IDydoyun_report_template_list.id
*/
private Long templateListId;
/**
* 最小阈值
*/
private BigDecimal minThreshold;
/**
* 最大阈值
*/
private BigDecimal maxThreshold;
/**
* 阈值定义
*/
private String thresholdDef;
/**
* 话术
*/
private String script;
/**
* 建议
*/
private String suggestion;
}

View 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 存储过程的参数 Mapkey 为参数名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() 返回 falseupdateCount: {}", 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>>;如果是 DMLINSERT/UPDATE/DELETE返回影响行数
*/
public Object executeSql(ReportDatabaseRespVO reportDatabase, String sql) {
log.info("开始执行 SQLdbHost={}, 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 原始数据 Mapkey 为驼峰字段名)
* @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 转换为 Mapkey 为属性名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);
}
}
}

View 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

View 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

View File

@@ -0,0 +1,3 @@
artifactId=mssql-jdbc
groupId=com.ydoyun
version=1.0.0

View File

@@ -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

View File

@@ -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

Binary file not shown.

Binary file not shown.