diff --git a/dsp/pom.xml b/dsp/pom.xml
index da6fe5e..52dc383 100644
--- a/dsp/pom.xml
+++ b/dsp/pom.xml
@@ -121,6 +121,11 @@
poi-ooxml
5.2.4
+
+ com.jcraft
+ jsch
+ 0.1.55
+
diff --git a/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java b/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java
new file mode 100644
index 0000000..ce877b2
--- /dev/null
+++ b/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java
@@ -0,0 +1,138 @@
+package com.jsc.dsp.utils;
+
+import com.jcraft.jsch.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+@Component
+public class SFTPConnector {
+
+ private static final Logger log = LoggerFactory.getLogger(SFTPConnector.class);
+
+ @Value("${sftp.host}")
+ private String host;
+
+ @Value("${sftp.port:22}") // SFTP 默认端口 22
+ private Integer port;
+
+ @Value("${sftp.username}")
+ private String username;
+
+ @Value("${sftp.password}") // 支持密码认证(生产环境建议改用私钥)
+ private String password;
+
+ @Value("${sftp.timeout:30000}")
+ private Integer timeout; // 单位:毫秒
+
+ @Value("${sftp.strictHostKeyChecking:false}") // false 仅用于测试环境!
+ private boolean strictHostKeyChecking;
+
+ /**
+ * 上传文件到 SFTP 服务器(密码认证)
+ *
+ * @param inputStream 源文件流(方法内部负责关闭)
+ * @param remotePath 远程绝对路径,如 /upload/2024/file.pdf
+ * @return 上传成功返回 true
+ */
+ public boolean uploadFile(InputStream inputStream, String remotePath) {
+ Session session = null;
+ ChannelSftp channelSftp = null;
+ try {
+ // 1. 初始化 JSch 会话
+ JSch jsch = new JSch();
+ session = jsch.getSession(username, host, port);
+ session.setPassword(password);
+ session.setTimeout(timeout);
+
+ // 2. 配置 SSH 连接参数(安全提示:生产环境必须启用 StrictHostKeyChecking 并配置 known_hosts)
+ Properties config = new Properties();
+ config.put("StrictHostKeyChecking", String.valueOf(strictHostKeyChecking));
+ session.setConfig(config);
+
+ // 3. 建立连接
+ session.connect();
+ channelSftp = (ChannelSftp) session.openChannel("sftp");
+ channelSftp.connect(timeout);
+
+ // 4. 确保目标目录存在
+ ensureDirectoryExists(channelSftp, remotePath);
+
+ // 5. 上传文件(JSch 会完整读取流,但不关闭流)
+ channelSftp.put(inputStream, remotePath);
+ log.info("SFTP 文件上传成功: {}", remotePath);
+ return true;
+
+ } catch (JSchException | SftpException e) {
+ log.error("SFTP 上传失败 [host={}, path={}]: {}", host, remotePath, e.getMessage(), e);
+ return false;
+ } catch (Exception e) {
+ log.error("SFTP 上传异常 [path={}]: {}", remotePath, e.getMessage(), e);
+ return false;
+ } finally {
+ // 6. 资源清理(先关流,再关通道/会话)
+ closeQuietly(inputStream);
+ if (channelSftp != null && channelSftp.isConnected()) {
+ try {
+ channelSftp.disconnect();
+ } catch (Exception e) {
+ log.warn("关闭 SFTP 通道异常", e);
+ }
+ }
+ if (session != null && session.isConnected()) {
+ session.disconnect();
+ }
+ }
+ }
+
+ /**
+ * 递归创建远程目录(基于 ChannelSftp)
+ *
+ * @param sftp SFTP 通道
+ * @param remotePath 完整远程文件路径(含文件名)
+ * @throws SftpException 目录创建失败时抛出
+ */
+ private void ensureDirectoryExists(ChannelSftp sftp, String remotePath) throws SftpException {
+ String dirPath = extractDirectory(remotePath);
+ if ("/".equals(dirPath)) return;
+
+ String[] dirs = dirPath.split("/");
+ StringBuilder current = new StringBuilder();
+ for (String dir : dirs) {
+ if (dir.isEmpty()) continue;
+ current.append("/").append(dir);
+ try {
+ sftp.cd(current.toString()); // 尝试进入目录
+ } catch (SftpException e) {
+ sftp.mkdir(current.toString()); // 不存在则创建
+ sftp.cd(current.toString());
+ }
+ }
+ }
+
+ /**
+ * 从完整路径提取目录部分(如 /a/b/file.txt → /a/b)
+ */
+ private String extractDirectory(String path) {
+ int lastSlash = path.lastIndexOf('/');
+ return (lastSlash <= 0) ? "/" : path.substring(0, lastSlash);
+ }
+
+ /**
+ * 安静关闭输入流
+ */
+ private void closeQuietly(InputStream is) {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ log.debug("关闭输入流时忽略异常", e);
+ }
+ }
+ }
+}
\ No newline at end of file