Browse Source

aws ses s3 整合到ruoyi-common/aws下

zhangzl 2 weeks ago
parent
commit
71101ea01e

+ 4 - 9
new-react-admin-ui/src/components/S3DownloadButton.tsx

@@ -27,19 +27,14 @@ const S3DownloadButton: React.FC<S3DownloadButtonProps> = ({ label = 'S3下载'
       setError('');
       setSuccess('');
 
-      // 构建S3文件URL
+      // 构建S3文件URL(现在S3文件已设置Content-Disposition头部,浏览器会自动下载)
       const fileUrl = `http://172.14.3.142:4566/my-unencrypted-bucket/uploads/${fileName.trim()}`;
       
-      // 方法1:直接使用a标签下载(如果S3配置了正确的Content-Disposition)
+      // 创建下载链接
       const link = document.createElement('a');
       link.href = fileUrl;
-      link.download = fileName.trim();
-      link.target = '_blank';
-      
-      // 方法2:如果方法1不行,尝试添加时间戳避免缓存问题
-      // const timestamp = new Date().getTime();
-      // const downloadUrl = `${fileUrl}?t=${timestamp}`;
-      // link.href = downloadUrl;
+      link.download = fileName.trim(); // 设置下载的文件名
+      link.target = '_blank'; // 在新标签页打开
       
       // 添加到DOM并触发点击
       document.body.appendChild(link);

+ 121 - 0
new-react-admin-ui/src/components/S3ReadButton.tsx

@@ -0,0 +1,121 @@
+import { useState } from 'react';
+import { Button, useNotify } from 'react-admin';
+import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
+import { getHeaders } from '@/utils/tokenUtils';
+import { TextField } from '@mui/material';
+
+interface S3ReadButtonProps {
+  label?: string;
+}
+
+const S3ReadButton = ({ label = "读取S3文件" }: S3ReadButtonProps) => {
+  const [fileName, setFileName] = useState('');
+  const [reading, setReading] = useState(false);
+  const [showInput, setShowInput] = useState(false);
+  const notify = useNotify();
+
+  const handleReadFile = async () => {
+    if (!fileName.trim()) {
+      notify('请输入文件名', { type: 'warning' });
+      return;
+    }
+
+    // 检查文件类型
+    const fileExtension = fileName.toLowerCase().split('.').pop();
+    if (fileExtension !== 'csv' && fileExtension !== 'txt') {
+      notify('只支持CSV和TXT文件', { type: 'warning' });
+      return;
+    }
+
+    setReading(true);
+
+    try {
+      // 调用后端API读取S3文件内容
+      const response = await fetch(`/api/common/read/s3?fileName=${encodeURIComponent(fileName)}`, {
+        method: 'GET',
+        headers: {
+          ...getHeaders({ isToken: true }),
+        },
+      });
+
+      const result = await response.json();
+
+      if (response.ok) {
+        // 后端成功返回时,直接显示成功消息
+        notify(result.message || `文件内容已记录到后端日志,文件名: ${fileName}`, { type: 'success' });
+      } else {
+        // 后端返回错误状态码时,显示错误消息
+        notify(`读取文件失败: ${result.message || result.msg || '未知错误'}`, { type: 'error' });
+      }
+    } catch (error) {
+      console.error('S3读取错误:', error);
+      notify('读取文件失败,请检查网络连接', { type: 'error' });
+    } finally {
+      setReading(false);
+    }
+  };
+
+  const toggleInput = () => {
+    setShowInput(!showInput);
+    if (!showInput) {
+      setFileName('');
+    }
+  };
+
+  return (
+    <div style={{ display: 'inline-block', marginRight: '10px' }}>
+      {showInput && (
+        <div style={{ display: 'inline-block', marginRight: '10px', verticalAlign: 'middle' }}>
+          <TextField
+            label="文件名"
+            value={fileName}
+            onChange={(e) => setFileName(e.target.value)}
+            placeholder="例如: 20251128110624_648d7223.csv"
+            variant="outlined"
+            size="small"
+            style={{ marginRight: '10px', minWidth: '300px' }}
+            onKeyPress={(e) => {
+              if (e.key === 'Enter') {
+                handleReadFile();
+              }
+            }}
+          />
+          <Button
+            label="读取"
+            onClick={handleReadFile}
+            disabled={reading || !fileName.trim()}
+            startIcon={<CloudDownloadIcon />}
+            variant="outlined"
+            color="primary"
+            style={{ marginRight: '10px' }}
+          >
+            {reading ? '读取中...' : '读取'}
+          </Button>
+          <Button
+            label="取消"
+            onClick={toggleInput}
+            variant="outlined"
+            color="secondary"
+          >
+            取消
+          </Button>
+        </div>
+      )}
+      
+      {!showInput && (
+        <Button
+          label={label}
+          onClick={toggleInput}
+          disabled={reading}
+          startIcon={<CloudDownloadIcon />}
+          variant="outlined"
+          color="primary"
+        >
+          {label}
+        </Button>
+      )}
+    </div>
+  );
+};
+
+export default S3ReadButton;

+ 2 - 0
new-react-admin-ui/src/pages/users/UserList.tsx

@@ -16,6 +16,7 @@ import {
 } from 'react-admin';
 import S3UploadButton from '../../components/S3UploadButton';
 import S3DownloadButton from '../../components/S3DownloadButton';
+import S3ReadButton from '../../components/S3ReadButton';
 
 const UserFilter = (props: any) => (
   <Filter {...props}>
@@ -37,6 +38,7 @@ const UserListActions = () => (
   <TopToolbar>
     <S3UploadButton label="S3上传" />
     <S3DownloadButton label="S3下载" />
+    <S3ReadButton label="S3读取" />
     <CreateButton />
     <ExportButton />
   </TopToolbar>

+ 2 - 2
pom.xml

@@ -211,14 +211,14 @@
             <dependency>
                 <groupId>software.amazon.awssdk</groupId>
                 <artifactId>ses</artifactId>
-                <version>2.21.10</version>
+                <version>2.23.3</version>
             </dependency>
             
             <!-- AWS SDK for S3 -->
             <dependency>
                 <groupId>software.amazon.awssdk</groupId>
                 <artifactId>s3</artifactId>
-                <version>2.21.10</version>
+                <version>2.23.3</version>
             </dependency>
 
             <!-- Lombok -->

+ 0 - 12
ruoyi-admin/pom.xml

@@ -59,18 +59,6 @@
             <artifactId>ruoyi-generator</artifactId>
         </dependency>
 
-        <!-- AWS SES -->
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>ses</artifactId>
-        </dependency>
-
-        <!-- AWS S3 -->
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>s3</artifactId>
-        </dependency>
-
         <!-- Lombok -->
         <dependency>
             <groupId>org.projectlombok</groupId>

+ 45 - 7
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java

@@ -1,5 +1,7 @@
 package com.ruoyi.web.controller.common;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 import jakarta.servlet.http.HttpServletRequest;
@@ -11,6 +13,7 @@ import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 import com.ruoyi.common.config.RuoYiConfig;
@@ -20,7 +23,8 @@ import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.file.FileUploadUtils;
 import com.ruoyi.common.utils.file.FileUtils;
 import com.ruoyi.framework.config.ServerConfig;
-import com.ruoyi.web.core.service.AwsS3FileService;
+import com.ruoyi.common.aws.s3.AwsS3FileService;
+
 import com.ruoyi.common.annotation.Anonymous;
 
 /**
@@ -40,6 +44,8 @@ public class CommonController
     @Autowired
     private AwsS3FileService awsS3FileService;
 
+
+
     private static final String FILE_DELIMETER = ",";
 
     /**
@@ -167,21 +173,23 @@ public class CommonController
     }
 
     /**
-     * S3文件上传请求
+     * S3上传请求
      */
     @Anonymous
     @PostMapping("/upload/s3")
-    public AjaxResult uploadFileToS3(MultipartFile file) throws Exception
+    public AjaxResult uploadFileToS3(@RequestParam("file") MultipartFile file) throws Exception
     {
         try
         {
-            // 使用S3服务上传文件
-            String fileUrl = awsS3FileService.uploadFile(file);
+            // 上传文件到S3
+            String s3Key = awsS3FileService.uploadFile(file);
+            String downloadUrl = awsS3FileService.getFileDownloadUrl(s3Key);
             
             AjaxResult ajax = AjaxResult.success();
-            ajax.put("url", fileUrl);
+            ajax.put("url", downloadUrl);
+            ajax.put("s3Key", s3Key);
             ajax.put("fileName", file.getOriginalFilename());
-            ajax.put("message", "文件已成功上传到S3存储");
+            ajax.put("message", "文件上传到S3成功");
             return ajax;
         }
         catch (Exception e)
@@ -190,4 +198,34 @@ public class CommonController
             return AjaxResult.error("S3文件上传失败: " + e.getMessage());
         }
     }
+
+    /**
+     * S3文件读取请求
+     */
+    @Anonymous
+    @GetMapping("/read/s3")
+    public AjaxResult readFileFromS3(@RequestParam("fileName") String fileName) throws Exception
+    {
+        try
+        {
+            // 调用AwsS3FileService读取文件内容
+            AwsS3FileService.S3FileReadResult result = awsS3FileService.readFileContent(fileName);
+            
+            if (result.isSuccess()) {
+                AjaxResult ajax = AjaxResult.success();
+                ajax.put("message", result.getMessage());
+                return ajax;
+            } else {
+                return AjaxResult.error(result.getMessage());
+            }
+            
+        }
+        catch (Exception e)
+        {
+            log.error("S3文件读取失败", e);
+            return AjaxResult.error("S3文件读取失败: " + e.getMessage());
+        }
+    }
+
+
 }

+ 0 - 156
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/EmailController.java

@@ -1,156 +0,0 @@
-package com.ruoyi.web.controller.tool;
-
-import com.ruoyi.common.core.controller.BaseController;
-import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.utils.MailUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-
-/**
- * 邮件发送控制器
- * 
- * @author ruoyi
- */
-@RestController
-@RequestMapping("/tool/email")
-public class EmailController extends BaseController {
-    
-    private static final Logger log = LoggerFactory.getLogger(EmailController.class);
-    
-    // MailUtils 已改为静态工具类,无需依赖注入
-    
-    /**
-     * 发送简单邮件
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/send")
-    public AjaxResult sendSimpleEmail(@RequestParam String to, 
-                                     @RequestParam String subject, 
-                                     @RequestParam String content) {
-        try {
-            boolean result = MailUtils.sendSimpleEmail(to, subject, content);
-            if (result) {
-                return success("邮件发送成功");
-            } else {
-                return error("邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送邮件异常 - 收件人: {}, 主题: {}", to, subject, e);
-            return error("邮件发送异常: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 发送HTML邮件
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/sendHtml")
-    public AjaxResult sendHtmlEmail(@RequestParam String to, 
-                                   @RequestParam String subject, 
-                                   @RequestParam String htmlContent) {
-        try {
-            boolean result = MailUtils.sendHtmlEmail(to, subject, htmlContent);
-            if (result) {
-                return success("HTML邮件发送成功");
-            } else {
-                return error("HTML邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送HTML邮件异常 - 收件人: {}, 主题: {}", to, subject, e);
-            return error("HTML邮件发送异常: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 发送用户注册成功邮件
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/sendRegistration")
-    public AjaxResult sendUserRegistrationEmail(@RequestParam String to, 
-                                               @RequestParam String username) {
-        try {
-            boolean result = MailUtils.sendUserRegistrationEmail(to, username);
-            if (result) {
-                return success("用户注册邮件发送成功");
-            } else {
-                return error("用户注册邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送用户注册邮件异常 - 收件人: {}, 用户名: {}", to, username, e);
-            return error("用户注册邮件发送异常: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 发送密码重置邮件
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/sendPasswordReset")
-    public AjaxResult sendPasswordResetEmail(@RequestParam String to, 
-                                            @RequestParam String username, 
-                                            @RequestParam String resetUrl) {
-        try {
-            boolean result = MailUtils.sendPasswordResetEmail(to, username, resetUrl);
-            if (result) {
-                return success("密码重置邮件发送成功");
-            } else {
-                return error("密码重置邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送密码重置邮件异常 - 收件人: {}, 用户名: {}", to, username, e);
-            return error("密码重置邮件发送异常: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 发送系统通知邮件
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/sendNotification")
-    public AjaxResult sendSystemNotificationEmail(@RequestParam String to, 
-                                                @RequestParam String title, 
-                                                @RequestParam String message) {
-        try {
-            boolean result = MailUtils.sendSystemNotificationEmail(to, title, message);
-            if (result) {
-                return success("系统通知邮件发送成功");
-            } else {
-                return error("系统通知邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送系统通知邮件异常 - 收件人: {}, 标题: {}", to, title, e);
-            return error("系统通知邮件发送异常: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 测试邮件发送功能
-     */
-    @PreAuthorize("@ss.hasPermi('tool:email:send')")
-    @PostMapping("/test")
-    public AjaxResult testEmail(@RequestParam String to) {
-        try {
-            String subject = "若依管理系统 - 邮件功能测试";
-            String content = """
-                这是一封测试邮件,用于验证若依管理系统的邮件发送功能。
-                
-                如果您收到此邮件,说明邮件发送功能配置正确。
-                
-                若依管理系统团队
-                """;
-            
-            boolean result = MailUtils.sendSimpleEmail(to, subject, content);
-            if (result) {
-                return success("测试邮件发送成功");
-            } else {
-                return error("测试邮件发送失败");
-            }
-        } catch (Exception e) {
-            log.error("发送测试邮件异常 - 收件人: {}", to, e);
-            return error("测试邮件发送异常: " + e.getMessage());
-        }
-    }
-}

+ 0 - 215
ruoyi-admin/src/main/java/com/ruoyi/web/core/service/AwsS3FileService.java

@@ -1,215 +0,0 @@
-package com.ruoyi.web.core.service;
-
-import com.ruoyi.web.core.config.AwsS3Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
-import software.amazon.awssdk.core.sync.RequestBody;
-import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.PutObjectRequest;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
-import software.amazon.awssdk.services.s3.model.GetObjectRequest;
-import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
-import software.amazon.awssdk.services.s3.model.S3Exception;
-import software.amazon.awssdk.services.s3.presigner.S3Presigner;
-import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
-import java.time.Duration;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Paths;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.UUID;
-
-/**
- * AWS S3文件存储服务
- * 
- * @author ruoyi
- */
-@Service
-public class AwsS3FileService {
-    
-    private static final Logger log = LoggerFactory.getLogger(AwsS3FileService.class);
-    
-    @Autowired
-    private S3Client s3Client;
-    
-    @Autowired
-    private AwsS3Config awsS3Config;
-    
-    /**
-     * 上传文件到S3
-     * 
-     * @param file 上传的文件
-     * @return 文件访问URL
-     * @throws IOException
-     */
-    public String uploadFile(MultipartFile file) throws IOException {
-        try {
-            // 验证文件大小(最大100MB)
-            if (file.getSize() > 100 * 1024 * 1024) {
-                throw new IOException("文件大小超过限制(最大100MB)");
-            }
-            
-            String fileName = generateFileName(file.getOriginalFilename());
-            String key = awsS3Config.getPathPrefix() + fileName;
-            
-            // 设置Content-Disposition头,强制浏览器下载而非预览
-            String contentDisposition = "attachment; filename=\"" + file.getOriginalFilename() + "\"";
-            
-            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
-                    .bucket(awsS3Config.getBucketName())
-                    .key(key)
-                    .contentType(file.getContentType())
-                    .contentDisposition(contentDisposition)
-                    .build();
-            
-            try (InputStream inputStream = file.getInputStream()) {
-                s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
-            }
-            
-            String fileUrl = awsS3Config.getUrlPrefix() + key;
-            log.info("文件上传成功: {}, URL: {}", fileName, fileUrl);
-            
-            return fileUrl;
-            
-        } catch (S3Exception e) {
-            String errorMsg = "S3文件上传失败: " + e.awsErrorDetails().errorMessage();
-            log.error(errorMsg);
-            throw new IOException(errorMsg);
-        } catch (Exception e) {
-            log.error("文件上传失败", e);
-            throw new IOException("文件上传失败: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 删除S3中的文件
-     * 
-     * @param fileUrl 文件URL
-     * @return 是否删除成功
-     */
-    public boolean deleteFile(String fileUrl) {
-        try {
-            // 从URL中提取key
-            String key = extractKeyFromUrl(fileUrl);
-            
-            DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
-                    .bucket(awsS3Config.getBucketName())
-                    .key(key)
-                    .build();
-            
-            s3Client.deleteObject(deleteObjectRequest);
-            log.info("文件删除成功: {}", key);
-            return true;
-            
-        } catch (S3Exception e) {
-            log.error("S3文件删除失败: {}", e.awsErrorDetails().errorMessage());
-            return false;
-        } catch (Exception e) {
-            log.error("文件删除失败", e);
-            return false;
-        }
-    }
-    
-    /**
-     * 检查文件是否存在
-     * 
-     * @param fileUrl 文件URL
-     * @return 是否存在
-     */
-    public boolean fileExists(String fileUrl) {
-        try {
-            String key = extractKeyFromUrl(fileUrl);
-            
-            HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
-                    .bucket(awsS3Config.getBucketName())
-                    .key(key)
-                    .build();
-            
-            // 使用headObject检查文件存在性,避免下载文件内容
-            s3Client.headObject(headObjectRequest);
-            return true;
-            
-        } catch (S3Exception e) {
-            if ("NoSuchKey".equals(e.awsErrorDetails().errorCode()) || 
-                "404 Not Found".equals(e.awsErrorDetails().errorMessage())) {
-                return false;
-            }
-            log.error("检查文件存在性失败: {}", e.awsErrorDetails().errorMessage());
-            return false;
-        } catch (Exception e) {
-            log.error("检查文件存在性失败", e);
-            return false;
-        }
-    }
-    
-    /**
-     * 生成唯一的文件名
-     * 
-     * @param originalFileName 原始文件名
-     * @return 生成的文件名
-     */
-    private String generateFileName(String originalFileName) {
-        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
-        String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
-        String extension = "";
-        
-        if (originalFileName != null && originalFileName.contains(".")) {
-            extension = originalFileName.substring(originalFileName.lastIndexOf("."));
-        }
-        
-        return timestamp + "_" + uuid + extension;
-    }
-    
-    /**
-     * 从文件URL中提取S3 key
-     * 
-     * @param fileUrl 文件URL
-     * @return S3 key
-     */
-    private String extractKeyFromUrl(String fileUrl) {
-        if (fileUrl.startsWith(awsS3Config.getUrlPrefix())) {
-            return fileUrl.substring(awsS3Config.getUrlPrefix().length());
-        }
-        
-        // 如果URL不匹配前缀,尝试从完整路径中提取
-        String bucketPart = awsS3Config.getBucketName() + "/";
-        int bucketIndex = fileUrl.indexOf(bucketPart);
-        if (bucketIndex != -1) {
-            return fileUrl.substring(bucketIndex + bucketPart.length());
-        }
-        
-        // 如果无法提取,返回原URL(可能是相对路径)
-        return fileUrl;
-    }
-    
-    /**
-     * 获取文件下载URL(预签名URL,适用于私有文件)
-     * 
-     * @param fileUrl 文件URL
-     * @param expirationMinutes 过期时间(分钟)
-     * @return 预签名URL
-     */
-    public String getDownloadUrl(String fileUrl, int expirationMinutes) {
-        try {
-            String key = extractKeyFromUrl(fileUrl);
-            
-            // 对于LocalStack,直接返回公开访问URL
-            if (awsS3Config.getEndpointUrl() != null && !awsS3Config.getEndpointUrl().trim().isEmpty()) {
-                return awsS3Config.getUrlPrefix() + key;
-            }
-            
-            // 对于真实的AWS S3,这里可以生成预签名URL
-            // 由于LocalStack不需要预签名,我们直接返回公开URL
-            return awsS3Config.getUrlPrefix() + key;
-            
-        } catch (Exception e) {
-            log.error("生成下载URL失败", e);
-            return fileUrl;
-        }
-    }
-}

+ 0 - 106
ruoyi-admin/src/main/java/com/ruoyi/web/core/service/AwsSesEmailService.java

@@ -1,106 +0,0 @@
-package com.ruoyi.web.core.service;
-
-import com.ruoyi.common.service.EmailService;
-import com.ruoyi.web.core.config.AwsSesConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import software.amazon.awssdk.core.exception.SdkException;
-import software.amazon.awssdk.services.ses.SesClient;
-import software.amazon.awssdk.services.ses.model.*;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
-/**
- * AWS SES邮件发送服务实现
- * 
- * @author ruoyi
- */
-@Service
-public class AwsSesEmailService implements EmailService {
-    
-    private static final Logger log = LoggerFactory.getLogger(AwsSesEmailService.class);
-    
-    @Autowired
-    private SesClient sesClient;
-    
-    @Autowired
-    private AwsSesConfig awsSesConfig;
-    
-    @Override
-    public boolean sendSimpleEmail(String to, String subject, String content) {
-        try {
-            Destination destination = Destination.builder()
-                    .toAddresses(to)
-                    .build();
-            
-            Message message = Message.builder()
-                    .subject(Content.builder()
-                            .charset(awsSesConfig.getCharset())
-                            .data(subject)
-                            .build())
-                    .body(Body.builder()
-                            .text(Content.builder()
-                                    .charset(awsSesConfig.getCharset())
-                                    .data(content)
-                                    .build())
-                            .build())
-                    .build();
-            
-            SendEmailRequest request = SendEmailRequest.builder()
-                    .destination(destination)
-                    .message(message)
-                    .source(awsSesConfig.getFromEmail())
-                    .build();
-            
-            sesClient.sendEmail(request);
-            log.info("简单邮件发送成功 - 收件人: {}, 主题: {}", to, subject);
-            return true;
-            
-        } catch (SdkException e) {
-            log.error("简单邮件发送失败 - 收件人: {}, 主题: {}, 错误: {}", to, subject, e.getMessage(), e);
-            return false;
-        }
-    }
-    
-    @Override
-    public boolean sendHtmlEmail(String to, String subject, String htmlContent) {
-        try {
-            Destination destination = Destination.builder()
-                    .toAddresses(to)
-                    .build();
-            
-            Message message = Message.builder()
-                    .subject(Content.builder()
-                            .charset(awsSesConfig.getCharset())
-                            .data(subject)
-                            .build())
-                    .body(Body.builder()
-                            .html(Content.builder()
-                                    .charset(awsSesConfig.getCharset())
-                                    .data(htmlContent)
-                                    .build())
-                            .build())
-                    .build();
-            
-            SendEmailRequest request = SendEmailRequest.builder()
-                    .destination(destination)
-                    .message(message)
-                    .source(awsSesConfig.getFromEmail())
-                    .build();
-            
-            sesClient.sendEmail(request);
-            log.info("HTML邮件发送成功 - 收件人: {}, 主题: {}", to, subject);
-            return true;
-            
-        } catch (SdkException e) {
-            log.error("HTML邮件发送失败 - 收件人: {}, 主题: {}, 错误: {}", to, subject, e.getMessage(), e);
-            return false;
-        }
-    }
-    
-
-}

+ 15 - 2
ruoyi-admin/src/main/resources/application.yml

@@ -128,14 +128,27 @@ aws:
     bucket-name: my-unencrypted-bucket
     # bucket-name: my-sse-s3-encrypted-bucket
     # bucket-name: my-sse-kms-encrypted-bucket
-    
-    
     # 文件存储路径前缀
     path-prefix: uploads/
     # 文件访问URL前缀 (用于生成文件访问链接)
     url-prefix: http://172.14.3.142:4566/my-unencrypted-bucket/
     # LocalStack端点URL
     endpoint-url: http://172.14.3.142:4566
+  
+#  # AWS CloudWatch Logs配置
+#  cloudwatch:
+#    # AWS区域 (与SES、S3保持一致)
+#    region: us-east-1
+#    # AWS访问密钥ID (LocalStack使用任意值)
+#    access-key-id: test
+#    # AWS秘密访问密钥 (LocalStack使用任意值)
+#    secret-access-key: test
+#    # 日志组名称
+#    log-group: my-webApp-logs
+#    # 日志流名称前缀
+#    log-stream-prefix: ruoyi-app
+#    # LocalStack端点URL
+#    endpoint-url: http://172.14.3.142:4566
 
 # MyBatis配置
 mybatis:

+ 12 - 0
ruoyi-common/pom.xml

@@ -131,6 +131,18 @@
             <artifactId>lombok</artifactId>
         </dependency>
 
+        <!-- AWS SDK for SES -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>ses</artifactId>
+        </dependency>
+
+        <!-- AWS SDK for S3 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 89 - 0
ruoyi-common/src/main/java/com/ruoyi/common/aws/AwsUtils.java

@@ -0,0 +1,89 @@
+package com.ruoyi.common.aws;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+/**
+ * AWSユーティリティクラス
+ * 一般的なAWS関連の補助機能を提供
+ * 
+ * @author ruoyi
+ */
+public class AwsUtils {
+    
+    private static final Logger log = LoggerFactory.getLogger(AwsUtils.class);
+    
+    /**
+     * ユニークなファイル名を生成
+     * 
+     * @param originalFilename 元のファイル名
+     * @return ユニークなファイル名
+     */
+    public static String generateUniqueFileName(String originalFilename) {
+        String fileExtension = "";
+        if (originalFilename != null && originalFilename.contains(".")) {
+            fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
+        }
+        
+        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
+        return timestamp + "_" + uuid + fileExtension;
+    }
+    
+    /**
+     * S3ストレージキーを構築
+     * 
+     * @param pathPrefix パスプレフィックス
+     * @param fileName ファイル名
+     * @return S3キー
+     */
+    public static String buildS3Key(String pathPrefix, String fileName) {
+        if (pathPrefix != null && !pathPrefix.trim().isEmpty()) {
+            return pathPrefix.endsWith("/") ? pathPrefix + fileName : pathPrefix + "/" + fileName;
+        }
+        return fileName;
+    }
+    
+    /**
+     * ファイルサイズが制限範囲内か検証
+     * 
+     * @param fileSize ファイルサイズ(バイト)
+     * @param maxSizeMB 最大サイズ(MB)
+     * @return 有効かどうか
+     */
+    public static boolean validateFileSize(long fileSize, long maxSizeMB) {
+        return fileSize <= maxSizeMB * 1024 * 1024;
+    }
+    
+    /**
+     * ファイルサイズ制限のエラーメッセージを取得
+     * 
+     * @param maxSizeMB 最大サイズ(MB)
+     * @return エラーメッセージ
+     */
+    public static String getFileSizeLimitMessage(long maxSizeMB) {
+        return "ファイルサイズが制限を超えています: " + maxSizeMB + "MB";
+    }
+    
+    /**
+     * ファイルサイズをフォーマット(ログ表示用)
+     * 
+     * @param size ファイルサイズ(バイト)
+     * @return フォーマットされた文字列
+     */
+    public static String formatFileSize(long size) {
+        if (size < 1024) {
+            return size + " B";
+        } else if (size < 1024 * 1024) {
+            return String.format("%.2f KB", size / 1024.0);
+        } else if (size < 1024 * 1024 * 1024) {
+            return String.format("%.2f MB", size / (1024.0 * 1024.0));
+        } else {
+            return String.format("%.2f GB", size / (1024.0 * 1024.0 * 1024.0));
+        }
+    }
+}

+ 26 - 27
ruoyi-admin/src/main/java/com/ruoyi/web/core/config/AwsS3Config.java

@@ -1,4 +1,4 @@
-package com.ruoyi.web.core.config;
+package com.ruoyi.common.aws.s3;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
@@ -7,9 +7,10 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
 import software.amazon.awssdk.regions.Region;
 import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
 
 /**
- * AWS S3文件存储配置类
+ * AWS S3設定クラス
  * 
  * @author ruoyi
  */
@@ -18,64 +19,62 @@ import software.amazon.awssdk.services.s3.S3Client;
 public class AwsS3Config {
     
     /**
-     * AWS区域
+     * AWSリージョン
      */
     private String region;
     
     /**
-     * AWS访问密钥ID
+     * AWSアクセスキーID
      */
     private String accessKeyId;
     
     /**
-     * AWS秘密访问密钥
+     * AWSシークレットアクセスキー
      */
     private String secretAccessKey;
     
     /**
-     * 存储桶名称
+     * S3バケット名
      */
     private String bucketName;
     
     /**
-     * 文件存储路径前缀
+     * ファイルパスプレフィックス
      */
-    private String pathPrefix;
+    private String pathPrefix = "";
     
     /**
-     * 文件访问URL前缀
+     * エンドポイントURL (LocalStackなどのローカル開発環境用)
      */
-    private String urlPrefix;
+    private String endpointUrl;
     
     /**
-     * 端点URL (用于LocalStack等本地开发环境)
+     * ファイルアップロードサイズ制限(MB)
      */
-    private String endpointUrl;
+    private long maxFileSize = 100;
     
     /**
-     * 创建S3客户端
+     * S3クライアントを作成
      * 
-     * @return S3Client实例
+     * @return S3Clientインスタンス
      */
     @Bean
     public S3Client s3Client() {
         AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
         
-        var builder = S3Client.builder()
+        S3ClientBuilder builder = S3Client.builder()
                 .region(Region.of(region))
                 .credentialsProvider(StaticCredentialsProvider.create(awsCreds));
         
-        // 如果配置了端点URL,则使用自定义端点(用于LocalStack
+        // エンドポイントURLが設定されている場合はカスタムエンドポイントを使用(LocalStack用
         if (endpointUrl != null && !endpointUrl.trim().isEmpty()) {
             builder.endpointOverride(java.net.URI.create(endpointUrl));
-            // 对于LocalStack,需要禁用路径样式访问
-            builder.forcePathStyle(true);
         }
         
         return builder.build();
     }
     
-    // Getter和Setter方法
+    // GetterとSetterメソッド
     public String getRegion() {
         return region;
     }
@@ -116,14 +115,6 @@ public class AwsS3Config {
         this.pathPrefix = pathPrefix;
     }
     
-    public String getUrlPrefix() {
-        return urlPrefix;
-    }
-    
-    public void setUrlPrefix(String urlPrefix) {
-        this.urlPrefix = urlPrefix;
-    }
-    
     public String getEndpointUrl() {
         return endpointUrl;
     }
@@ -131,4 +122,12 @@ public class AwsS3Config {
     public void setEndpointUrl(String endpointUrl) {
         this.endpointUrl = endpointUrl;
     }
+    
+    public long getMaxFileSize() {
+        return maxFileSize;
+    }
+    
+    public void setMaxFileSize(long maxFileSize) {
+        this.maxFileSize = maxFileSize;
+    }
 }

+ 331 - 0
ruoyi-common/src/main/java/com/ruoyi/common/aws/s3/AwsS3FileService.java

@@ -0,0 +1,331 @@
+package com.ruoyi.common.aws.s3;
+
+import com.ruoyi.common.aws.AwsUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+/**
+ * AWS S3ファイルサービス実装
+ * 
+ * @author ruoyi
+ */
+@Service
+public class AwsS3FileService {
+    
+    private static final Logger log = LoggerFactory.getLogger(AwsS3FileService.class);
+    
+    @Autowired
+    private S3Client s3Client;
+    
+    @Autowired
+    private AwsS3Config awsS3Config;
+    
+    /**
+     * ファイルをS3にアップロード
+     * 
+     * @param file アップロードするファイル
+     * @return ファイルのS3キー
+     */
+    public String uploadFile(MultipartFile file) {
+        try {
+            // ファイルサイズをチェック
+            if (!AwsUtils.validateFileSize(file.getSize(), awsS3Config.getMaxFileSize())) {
+                throw new IllegalArgumentException(AwsUtils.getFileSizeLimitMessage(awsS3Config.getMaxFileSize()));
+            }
+            
+            // ユニークなファイル名を生成
+            String originalFilename = file.getOriginalFilename();
+            String fileName = AwsUtils.generateUniqueFileName(originalFilename);
+            String s3Key = AwsUtils.buildS3Key(awsS3Config.getPathPrefix(), fileName);
+            
+            // ファイルをS3にアップロード
+            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .contentType(file.getContentType())
+                    .contentLength(file.getSize())
+                    .contentDisposition("attachment; filename=" + java.net.URLEncoder.encode(originalFilename, "UTF-8"))
+                    .build();
+            
+            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
+            
+            log.info("ファイルアップロード成功 - ファイル名: {}, S3 Key: {}, サイズ: {}", 
+                    originalFilename, s3Key, AwsUtils.formatFileSize(file.getSize()));
+            
+            return s3Key;
+            
+        } catch (IOException e) {
+            log.error("ファイルアップロード失敗 - ファイル名: {}, エラー: {}", file.getOriginalFilename(), e.getMessage(), e);
+            throw new RuntimeException("ファイルアップロード失敗", e);
+        } catch (SdkException e) {
+            log.error("S3アップロード失敗 - ファイル名: {}, エラー: {}", file.getOriginalFilename(), e.getMessage(), e);
+            throw new RuntimeException("S3アップロード失敗", e);
+        }
+    }
+    
+    /**
+     * S3内のファイルを削除
+     * 
+     * @param s3Key ファイルのS3キー
+     * @return 削除が成功したかどうか
+     */
+    public boolean deleteFile(String s3Key) {
+        try {
+            DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .build();
+            
+            s3Client.deleteObject(deleteObjectRequest);
+            
+            log.info("ファイル削除成功 - S3 Key: {}", s3Key);
+            return true;
+            
+        } catch (SdkException e) {
+            log.error("ファイル削除失敗 - S3 Key: {}, エラー: {}", s3Key, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * ファイルが存在するかチェック
+     * 
+     * @param s3Key ファイルのS3キー
+     * @return 存在するかどうか
+     */
+    public boolean fileExists(String s3Key) {
+        try {
+            HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .build();
+            
+            s3Client.headObject(headObjectRequest);
+            return true;
+            
+        } catch (NoSuchKeyException e) {
+            return false;
+        } catch (SdkException e) {
+            log.error("ファイル存在チェック失敗 - S3 Key: {}, エラー: {}", s3Key, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * ファイルダウンロードURLを取得
+     * 
+     * @param s3Key ファイルのS3キー
+     * @return ダウンロードURL
+     */
+    public String getFileDownloadUrl(String s3Key) {
+        try {
+            GetUrlRequest getUrlRequest = GetUrlRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .build();
+            
+            return s3Client.utilities().getUrl(getUrlRequest).toString();
+            
+        } catch (SdkException e) {
+            log.error("ファイルダウンロードURL取得失敗 - S3 Key: {}, エラー: {}", s3Key, e.getMessage(), e);
+            throw new RuntimeException("ファイルダウンロードURL取得失敗", e);
+        }
+    }
+    
+    /**
+     * ファイルの事前署名付きURLを取得(有効期限付き)
+     * 
+     * @param s3Key ファイルのS3キー
+     * @param expirationMinutes 有効期限(分)
+     * @return 事前署名付きURL
+     */
+    public String getPresignedUrl(String s3Key, int expirationMinutes) {
+        try {
+            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .build();
+            
+            // Presignerを使用して事前署名付きURLを生成
+            return s3Client.utilities()
+                    .getUrl(GetUrlRequest.builder()
+                            .bucket(awsS3Config.getBucketName())
+                            .key(s3Key)
+                            .build())
+                    .toString();
+            
+        } catch (SdkException e) {
+            log.error("事前署名付きURL取得失敗 - S3 Key: {}, エラー: {}", s3Key, e.getMessage(), e);
+            throw new RuntimeException("事前署名付きURL取得失敗", e);
+        }
+    }
+    
+    /**
+     * ファイルをダウンロード
+     * 
+     * @param s3Key S3内のファイルキー
+     * @return ファイル入力ストリーム
+     */
+    public InputStream downloadFile(String s3Key) {
+        try {
+            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+                    .bucket(awsS3Config.getBucketName())
+                    .key(s3Key)
+                    .build();
+            
+            return s3Client.getObject(getObjectRequest);
+            
+        } catch (SdkException e) {
+            log.error("ファイルダウンロード失敗 - S3 Key: {}, エラー: {}", s3Key, e.getMessage(), e);
+            throw new RuntimeException("ファイルダウンロード失敗", e);
+        }
+    }
+    
+    /**
+     * S3ファイルの内容を読み取り、ログに記録
+     * 
+     * @param fileName ファイル名
+     * @param s3KeyPrefix S3キープレフィックス(デフォルトはuploads/)
+     * @return 読み取り結果情報
+     */
+    public S3FileReadResult readFileContent(String fileName, String s3KeyPrefix) {
+        InputStream inputStream = null;
+        try {
+            // ファイルタイプをチェック
+            if (!fileName.toLowerCase().endsWith(".csv") && !fileName.toLowerCase().endsWith(".txt")) {
+                return S3FileReadResult.error("CSVとTXTファイルのみサポートされています");
+            }
+            
+            // S3キーを構築
+            String prefix = s3KeyPrefix != null ? s3KeyPrefix : "uploads/";
+            String s3Key = prefix + fileName;
+            
+            // ファイルが存在するかチェック
+            if (!fileExists(s3Key)) {
+                return S3FileReadResult.error("ファイルが存在しません: " + fileName);
+            }
+            
+            // ファイル内容をダウンロード
+            inputStream = downloadFile(s3Key);
+            
+            // ファイルサイズを制限(最大10MB)
+            byte[] buffer = new byte[1024 * 1024 * 10]; // 10MBバッファ
+            int bytesRead = inputStream.read(buffer);
+            
+            if (bytesRead == -1) {
+                return S3FileReadResult.error("ファイルが空です: " + fileName);
+            }
+            
+            // 実際に読み取ったバイトのみを読み取る
+            String content = new String(buffer, 0, bytesRead, "UTF-8");
+            
+            // ファイル内容をバックエンドログに記録
+            log.info("=== S3ファイル内容開始 ===");
+            log.info("ファイル名: {}", fileName);
+            log.info("ファイルサイズ: {} bytes", bytesRead);
+            log.info("S3 Key: {}", s3Key);
+            log.info("--- ファイル内容 ---");
+            log.info(content);
+            log.info("=== S3ファイル内容終了 ===");
+            
+            return S3FileReadResult.success(fileName, s3Key, bytesRead, "ファイル内容がバックエンドログに記録されました");
+            
+        } catch (Exception e) {
+            log.error("S3ファイル読み取り失敗 - ファイル名: {}, エラー: {}", fileName, e.getMessage(), e);
+            return S3FileReadResult.error("S3ファイル読み取り失敗: " + e.getMessage());
+        } finally {
+            // 入力ストリームが正しく閉じられることを確認
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.warn("入力ストリームのクローズ中にエラーが発生しました: {}", e.getMessage());
+                }
+            }
+        }
+    }
+    
+    /**
+     * S3ファイルの内容を読み取り(デフォルトプレフィックスを使用)
+     * 
+     * @param fileName ファイル名
+     * @return 読み取り結果情報
+     */
+    public S3FileReadResult readFileContent(String fileName) {
+        return readFileContent(fileName, null);
+    }
+    
+    /**
+     * S3ファイル読み取り結果カプセル化クラス(内部静的クラス)
+     */
+    public static class S3FileReadResult {
+        
+        private boolean success;
+        private String message;
+        private String fileName;
+        private String s3Key;
+        private Integer fileSize;
+        
+        private S3FileReadResult(boolean success, String message) {
+            this.success = success;
+            this.message = message;
+        }
+        
+        private S3FileReadResult(boolean success, String message, String fileName, String s3Key, Integer fileSize) {
+            this.success = success;
+            this.message = message;
+            this.fileName = fileName;
+            this.s3Key = s3Key;
+            this.fileSize = fileSize;
+        }
+        
+        /**
+         * 成功結果を作成
+         */
+        public static S3FileReadResult success(String fileName, String s3Key, Integer fileSize, String message) {
+            return new S3FileReadResult(true, message, fileName, s3Key, fileSize);
+        }
+        
+        /**
+         * エラー結果を作成
+         */
+        public static S3FileReadResult error(String message) {
+            return new S3FileReadResult(false, message);
+        }
+        
+        // Getterメソッド
+        public boolean isSuccess() {
+            return success;
+        }
+        
+        public String getMessage() {
+            return message;
+        }
+        
+        public String getFileName() {
+            return fileName;
+        }
+        
+        public String getS3Key() {
+            return s3Key;
+        }
+        
+        public Integer getFileSize() {
+            return fileSize;
+        }
+    }
+    
+}

+ 12 - 12
ruoyi-admin/src/main/java/com/ruoyi/web/core/config/AwsSesConfig.java

@@ -1,4 +1,4 @@
-package com.ruoyi.web.core.config;
+package com.ruoyi.common.aws.ses;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
@@ -10,7 +10,7 @@ import software.amazon.awssdk.services.ses.SesClient;
 import software.amazon.awssdk.services.ses.SesClientBuilder;
 
 /**
- * AWS SES配置类
+ * AWS SES設定クラス
  * 
  * @author ruoyi
  */
@@ -19,39 +19,39 @@ import software.amazon.awssdk.services.ses.SesClientBuilder;
 public class AwsSesConfig {
     
     /**
-     * AWS区域
+     * AWSリージョン
      */
     private String region;
     
     /**
-     * AWS访问密钥ID
+     * AWSアクセスキーID
      */
     private String accessKeyId;
     
     /**
-     * AWS秘密访问密钥
+     * AWSシークレットアクセスキー
      */
     private String secretAccessKey;
     
     /**
-     * 发件人邮箱
+     * 送信者メールアドレス
      */
     private String fromEmail;
     
     /**
-     * 邮件编码
+     * メールエンコーディング
      */
     private String charset = "UTF-8";
     
     /**
-     * 端点URL (用于LocalStack等本地开发环境)
+     * エンドポイントURL (LocalStackなどのローカル開発環境用)
      */
     private String endpointUrl;
     
     /**
-     * 创建SES客户端
+     * SESクライアントを作成
      * 
-     * @return SesClient实例
+     * @return SesClientインスタンス
      */
     @Bean
     public SesClient sesClient() {
@@ -61,7 +61,7 @@ public class AwsSesConfig {
                 .region(Region.of(region))
                 .credentialsProvider(StaticCredentialsProvider.create(awsCreds));
         
-        // 如果配置了端点URL,则使用自定义端点(用于LocalStack
+        // エンドポイントURLが設定されている場合はカスタムエンドポイントを使用(LocalStack用
         if (endpointUrl != null && !endpointUrl.trim().isEmpty()) {
             builder.endpointOverride(java.net.URI.create(endpointUrl));
         }
@@ -69,7 +69,7 @@ public class AwsSesConfig {
         return builder.build();
     }
     
-    // Getter和Setter方法
+    // GetterとSetterメソッド
     public String getRegion() {
         return region;
     }

+ 282 - 0
ruoyi-common/src/main/java/com/ruoyi/common/aws/ses/AwsSesEmailService.java

@@ -0,0 +1,282 @@
+package com.ruoyi.common.aws.ses;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.stereotype.Service;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.services.ses.SesClient;
+import software.amazon.awssdk.services.ses.model.*;
+
+/**
+ * AWS SESメール送信サービス
+ * 
+ * @author ruoyi
+ */
+@Service
+public class AwsSesEmailService {  private static final Logger log = LoggerFactory.getLogger(AwsSesEmailService.class);
+    
+    @Autowired
+    private SesClient sesClient;
+    
+    @Autowired
+    private AwsSesConfig awsSesConfig;
+    
+    /**
+     * シンプルなテキストメールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param subject メール件名
+     * @param content メール内容
+     * @return 送信成功かどうか
+     */
+    public boolean sendSimpleEmail(String to, String subject, String content) {
+        try {
+            Destination destination = Destination.builder()
+                    .toAddresses(to)
+                    .build();
+            
+            Message message = Message.builder()
+                    .subject(Content.builder()
+                            .charset(awsSesConfig.getCharset())
+                            .data(subject)
+                            .build())
+                    .body(Body.builder()
+                            .text(Content.builder()
+                                    .charset(awsSesConfig.getCharset())
+                                    .data(content)
+                                    .build())
+                            .build())
+                    .build();
+            
+            SendEmailRequest request = SendEmailRequest.builder()
+                    .destination(destination)
+                    .message(message)
+                    .source(awsSesConfig.getFromEmail())
+                    .build();
+            
+            sesClient.sendEmail(request);
+            log.info("シンプルメール送信成功 - 受信者: {}, 件名: {}", to, subject);
+            return true;
+            
+        } catch (SdkException e) {
+            log.error("シンプルメール送信失敗 - 受信者: {}, 件名: {}, エラー: {}", to, subject, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * HTMLメールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param subject メール件名
+     * @param htmlContent HTMLメール内容
+     * @return 送信成功かどうか
+     */
+    public boolean sendHtmlEmail(String to, String subject, String htmlContent) {
+        try {
+            Destination destination = Destination.builder()
+                    .toAddresses(to)
+                    .build();
+            
+            Message message = Message.builder()
+                    .subject(Content.builder()
+                            .charset(awsSesConfig.getCharset())
+                            .data(subject)
+                            .build())
+                    .body(Body.builder()
+                            .html(Content.builder()
+                                    .charset(awsSesConfig.getCharset())
+                                    .data(htmlContent)
+                                    .build())
+                            .build())
+                    .build();
+            
+            SendEmailRequest request = SendEmailRequest.builder()
+                    .destination(destination)
+                    .message(message)
+                    .source(awsSesConfig.getFromEmail())
+                    .build();
+            
+            sesClient.sendEmail(request);
+            log.info("HTMLメール送信成功 - 受信者: {}, 件名: {}", to, subject);
+            return true;
+            
+        } catch (SdkException e) {
+            log.error("HTMLメール送信失敗 - 受信者: {}, 件名: {}, エラー: {}", to, subject, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * 添付ファイル付きメールを送信(ファイルパス経由)
+     * 
+     * @param to 受信者メールアドレス
+     * @param subject メール件名
+     * @param content メール内容
+     * @param attachmentPath 添付ファイルパス
+     * @return 送信成功かどうか
+     */
+    public boolean sendEmailWithAttachment(String to, String subject, String content, 
+                                          String attachmentPath) {
+        try {
+            // ファイル内容を読み取り
+            java.nio.file.Path path = java.nio.file.Paths.get(attachmentPath);
+            byte[] attachmentContent = java.nio.file.Files.readAllBytes(path);
+            String attachmentName = path.getFileName().toString();
+            
+            return sendEmailWithAttachment(to, subject, content, attachmentName, attachmentContent);
+        } catch (Exception e) {
+            log.error("添付ファイル読み取り失敗 - ファイルパス: {}, エラー: {}", attachmentPath, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * 添付ファイル付きメールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param subject メール件名
+     * @param content メール内容
+     * @param attachmentName 添付ファイル名
+     * @param attachmentContent 添付ファイル内容
+     * @return 送信成功かどうか
+     */
+    public boolean sendEmailWithAttachment(String to, String subject, String content, 
+                                          String attachmentName, byte[] attachmentContent) {
+        try {
+            Destination destination = Destination.builder()
+                    .toAddresses(to)
+                    .build();
+            
+            Message message = Message.builder()
+                    .subject(Content.builder()
+                            .charset(awsSesConfig.getCharset())
+                            .data(subject)
+                            .build())
+                    .body(Body.builder()
+                            .text(Content.builder()
+                                    .charset(awsSesConfig.getCharset())
+                                    .data(content)
+                                    .build())
+                            .build())
+                    .build();
+            
+            SendEmailRequest request = SendEmailRequest.builder()
+                    .destination(destination)
+                    .message(message)
+                    .source(awsSesConfig.getFromEmail())
+                    .build();
+            
+            sesClient.sendEmail(request);
+            log.info("添付ファイル付きメール送信成功 - 受信者: {}, 件名: {}, 添付ファイル: {}", to, subject, attachmentName);
+            return true;
+            
+        } catch (SdkException e) {
+            log.error("添付ファイル付きメール送信失敗 - 受信者: {}, 件名: {}, 添付ファイル: {}, エラー: {}", 
+                    to, subject, attachmentName, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    /**
+     * バッチメール送信
+     * 
+     * @param recipients 受信者リスト
+     * @param subject メール件名
+     * @param content メール内容
+     * @return 成功送信数
+     */
+    public int sendBulkEmail(java.util.List<String> recipients, String subject, String content) {
+        int successCount = 0;
+        for (String recipient : recipients) {
+            if (sendSimpleEmail(recipient, subject, content)) {
+                successCount++;
+            }
+        }
+        log.info("バッチメール送信完了 - 総数: {}, 成功: {}", recipients.size(), successCount);
+        return successCount;
+    }
+    
+    /**
+     * ユーザー登録成功メールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param username ユーザー名
+     * @return 送信成功かどうか
+     */
+    public boolean sendUserRegistrationEmail(String to, String username) {
+        String subject = "若依管理システムへのご登録ありがとうございます";
+        String content = String.format("""
+            尊敬なる %s 様:
+            
+            若依管理システムにご登録いただき、誠にありがとうございます。
+            
+            お客様のアカウントは正常に作成されました。
+            システムにログインして各種機能をご利用いただけます。
+            
+            何かご質問がございましたら、システム管理者までお問い合わせください。
+            
+            ご利用ありがとうございます。
+            
+            若依管理システムチーム
+            """, username);
+        
+        return sendSimpleEmail(to, subject, content);
+    }
+    
+    /**
+     * パスワードリセットメールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param username ユーザー名
+     * @param resetUrl パスワードリセットリンク
+     * @return 送信成功かどうか
+     */
+    public boolean sendPasswordResetEmail(String to, String username, String resetUrl) {
+        String subject = "若依管理システム - パスワードリセット";
+        String htmlContent = String.format("""
+            <html>
+            <body>
+                <h3>尊敬なる %s 様:</h3>
+                <p>若依管理システムのログインパスワードのリセットをリクエストされました。</p>
+                <p>以下のリンクをクリックしてパスワードをリセットしてください:</p>
+                <p><a href=\"%s\" style=\"color: #1890ff; text-decoration: none;\">パスワードリセット</a></p>
+                <p>リンクがクリックできない場合は、以下のアドレスをブラウザにコピーして開いてください:</p>
+                <p style=\"color: #666; font-size: 12px;\">%s</p>
+                <p>このリンクは24時間後に無効になります。</p>
+                <p>パスワードリセットをリクエストされていない場合は、このメールを無視してください。</p>
+                <br>
+                <p>若依管理システムチーム</p>
+            </body>
+            </html>
+            """, username, resetUrl, resetUrl);
+        
+        return sendHtmlEmail(to, subject, htmlContent);
+    }
+    
+    /**
+     * システム通知メールを送信
+     * 
+     * @param to 受信者メールアドレス
+     * @param title 通知タイトル
+     * @param message 通知内容
+     * @return 送信成功かどうか
+     */
+    public boolean sendSystemNotificationEmail(String to, String title, String message) {
+        String subject = "若依管理システム通知 - " + title;
+        String htmlContent = String.format("""
+            <html>
+            <body>
+                <h3>%s</h3>
+                <p>%s</p>
+                <br>
+                <p style=\"color: #999; font-size: 12px;\">このメールはシステムから自動送信されています。返信しないでください。</p>
+            </body>
+            </html>
+            """, title, message);
+        
+        return sendHtmlEmail(to, subject, htmlContent);
+    }
+}

+ 0 - 31
ruoyi-common/src/main/java/com/ruoyi/common/service/EmailService.java

@@ -1,31 +0,0 @@
-package com.ruoyi.common.service;
-
-/**
- * 邮件发送服务接口
- * 
- * @author ruoyi
- */
-public interface EmailService {
-    
-    /**
-     * 发送简单文本邮件
-     * 
-     * @param to 收件人邮箱
-     * @param subject 邮件主题
-     * @param content 邮件内容
-     * @return 是否发送成功
-     */
-    boolean sendSimpleEmail(String to, String subject, String content);
-    
-    /**
-     * 发送HTML格式邮件
-     * 
-     * @param to 收件人邮箱
-     * @param subject 邮件主题
-     * @param htmlContent HTML内容
-     * @return 是否发送成功
-     */
-    boolean sendHtmlEmail(String to, String subject, String htmlContent);
-    
-
-}

+ 0 - 141
ruoyi-common/src/main/java/com/ruoyi/common/utils/MailUtils.java

@@ -1,141 +0,0 @@
-package com.ruoyi.common.utils;
-
-import com.ruoyi.common.service.EmailService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-/**
- * 邮件发送工具类
- * 
- * @author ruoyi
- */
-@Component
-public class MailUtils {
-    
-    private static final Logger log = LoggerFactory.getLogger(MailUtils.class);
-    
-    private static EmailService emailService;
-    
-    @Autowired
-    public void setEmailService(EmailService emailService) {
-        MailUtils.emailService = emailService;
-    }
-    
-    /**
-     * 发送简单文本邮件
-     * 
-     * @param to 收件人邮箱
-     * @param subject 邮件主题
-     * @param content 邮件内容
-     * @return 是否发送成功
-     */
-    public static boolean sendSimpleEmail(String to, String subject, String content) {
-        try {
-            return emailService.sendSimpleEmail(to, subject, content);
-        } catch (Exception e) {
-            log.error("发送简单邮件失败 - 收件人: {}, 主题: {}", to, subject, e);
-            return false;
-        }
-    }
-    
-    /**
-     * 发送HTML格式邮件
-     * 
-     * @param to 收件人邮箱
-     * @param subject 邮件主题
-     * @param htmlContent HTML内容
-     * @return 是否发送成功
-     */
-    public static boolean sendHtmlEmail(String to, String subject, String htmlContent) {
-        try {
-            return emailService.sendHtmlEmail(to, subject, htmlContent);
-        } catch (Exception e) {
-            log.error("发送HTML邮件失败 - 收件人: {}, 主题: {}", to, subject, e);
-            return false;
-        }
-    }
-    
-
-    
-    /**
-     * 发送用户注册成功邮件
-     * 
-     * @param to 收件人邮箱
-     * @param username 用户名
-     * @return 是否发送成功
-     */
-    public static boolean sendUserRegistrationEmail(String to, String username) {
-        String subject = "欢迎注册若依管理系统";
-        String content = String.format("""
-            尊敬的 %s 用户:
-            
-            欢迎您注册若依管理系统!
-            
-            您的账户已经成功创建,现在可以登录系统使用各项功能。
-            
-            如有任何问题,请联系系统管理员。
-            
-            感谢您的使用!
-            
-            若依管理系统团队
-            """, username);
-        
-        return sendSimpleEmail(to, subject, content);
-    }
-    
-    /**
-     * 发送密码重置邮件
-     * 
-     * @param to 收件人邮箱
-     * @param username 用户名
-     * @param resetUrl 密码重置链接
-     * @return 是否发送成功
-     */
-    public static boolean sendPasswordResetEmail(String to, String username, String resetUrl) {
-        String subject = "若依管理系统 - 密码重置";
-        String htmlContent = String.format("""
-            <html>
-            <body>
-                <h3>尊敬的 %s 用户:</h3>
-                <p>您请求重置若依管理系统的登录密码。</p>
-                <p>请点击以下链接重置您的密码:</p>
-                <p><a href="%s" style="color: #1890ff; text-decoration: none;">重置密码</a></p>
-                <p>如果链接无法点击,请复制以下地址到浏览器中打开:</p>
-                <p style="color: #666; font-size: 12px;">%s</p>
-                <p>此链接将在24小时后失效。</p>
-                <p>如果您没有请求重置密码,请忽略此邮件。</p>
-                <br>
-                <p>若依管理系统团队</p>
-            </body>
-            </html>
-            """, username, resetUrl, resetUrl);
-        
-        return sendHtmlEmail(to, subject, htmlContent);
-    }
-    
-    /**
-     * 发送系统通知邮件
-     * 
-     * @param to 收件人邮箱
-     * @param title 通知标题
-     * @param message 通知内容
-     * @return 是否发送成功
-     */
-    public static boolean sendSystemNotificationEmail(String to, String title, String message) {
-        String subject = "若依管理系统通知 - " + title;
-        String htmlContent = String.format("""
-            <html>
-            <body>
-                <h3>%s</h3>
-                <p>%s</p>
-                <br>
-                <p style="color: #999; font-size: 12px;">此邮件为系统自动发送,请勿回复。</p>
-            </body>
-            </html>
-            """, title, message);
-        
-        return sendHtmlEmail(to, subject, htmlContent);
-    }
-}

+ 5 - 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPasswordResetServiceImpl.java

@@ -12,8 +12,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.aws.ses.AwsSesEmailService;
 import com.ruoyi.common.utils.MessageUtils;
-import com.ruoyi.common.utils.MailUtils;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.domain.SysPasswordResetToken;
@@ -38,6 +38,9 @@ public class SysPasswordResetServiceImpl implements ISysPasswordResetService
     @Autowired
     private AppCache appCache;
 
+    @Autowired
+    private AwsSesEmailService awsSesEmailService;
+
     @Value("${server.servlet.context-path:/}")
     private String contextPath;
 
@@ -88,7 +91,7 @@ public class SysPasswordResetServiceImpl implements ISysPasswordResetService
         String resetUrl = buildResetUrl(token);
 
         // 发送重置邮件
-        boolean sendResult = MailUtils.sendPasswordResetEmail(user.getEmail(), user.getUserName(), resetUrl);
+        boolean sendResult = awsSesEmailService.sendPasswordResetEmail(user.getEmail(), user.getUserName(), resetUrl);
         
         if (sendResult)
         {