Browse Source

忘记密码功能 需等待AWS SES邮件配置

zhangzl 3 weeks ago
parent
commit
7821f28b9c

+ 4 - 0
new-react-admin-ui/src/App.tsx

@@ -4,6 +4,8 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
 import { ruoyiDataProvider } from '@/adapters/ruoyiDataProvider';
 import { ruoyiAuthProvider } from '@/adapters/ruoyiAuthProvider';
 import { LoginPage } from '@/pages/LoginPage';
+import { ForgotPasswordPage } from '@/pages/ForgotPasswordPage';
+import { ResetPasswordPage } from '@/pages/ResetPasswordPage';
 import { UserList } from '@/pages/users/UserList';
 import { UserEdit } from '@/pages/users/UserEdit';
 import { UserCreate } from '@/pages/users/UserCreate';
@@ -69,6 +71,8 @@ export const App = () => {
         <CustomRoutes noLayout>
           <Route path="/error" element={<ErrorPage />} />
           <Route path="/tool/swagger" element={<SwaggerPage />} />
+          <Route path="/forgot-password" element={<ForgotPasswordPage />} />
+          <Route path="/reset-password" element={<ResetPasswordPage />} />
         </CustomRoutes>
         
         {/* Dashboard通过Resource自动路由,无需CustomRoutes */}

+ 162 - 0
new-react-admin-ui/src/pages/ForgotPasswordPage.tsx

@@ -0,0 +1,162 @@
+import { useState } from 'react';
+import { useNotify } from 'react-admin';
+import {
+  Box,
+  Button,
+  TextField,
+  Typography,
+  Container,
+  Paper,
+  Link,
+  CircularProgress,
+} from '@mui/material';
+import { ArrowBack, Email } from '@mui/icons-material';
+
+// 密码重置API调用函数
+const requestPasswordReset = async (username: string) => {
+  const response = await fetch('/system/password/request', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({ username }),
+  });
+  
+  if (!response.ok) {
+    throw new Error('请求失败');
+  }
+  
+  return response.json();
+};
+
+export const ForgotPasswordPage = () => {
+  const [username, setUsername] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [submitted, setSubmitted] = useState(false);
+  const notify = useNotify();
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    
+    if (!username) {
+      notify('请输入用户名', { type: 'warning' });
+      return;
+    }
+
+    setLoading(true);
+    
+    try {
+      const result = await requestPasswordReset(username);
+      if (result.code === 200) {
+        setSubmitted(true);
+        notify(result.msg || '密码重置邮件已发送,请查收您的邮箱', { type: 'success' });
+      } else {
+        notify(result.msg || '请求失败,请稍后重试', { type: 'error' });
+      }
+    } catch (error) {
+      notify(error instanceof Error ? error.message : '请求失败,请检查网络连接', { type: 'error' });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleBackToLogin = () => {
+    window.location.href = '/login';
+  };
+
+  return (
+    <Container component="main" maxWidth="sm">
+      <Box
+        sx={{
+          marginTop: 8,
+          display: 'flex',
+          flexDirection: 'column',
+          alignItems: 'center',
+        }}
+      >
+        <Paper
+          elevation={3}
+          sx={{
+            padding: 4,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+            width: '100%',
+          }}
+        >
+          <Email sx={{ fontSize: 40, mb: 2, color: 'primary.main' }} />
+          <Typography component="h1" variant="h4" gutterBottom>
+            找回密码
+          </Typography>
+          
+          {!submitted ? (
+            <>
+              <Typography variant="body2" color="text.secondary" sx={{ mb: 3, textAlign: 'center' }}>
+                请输入您的用户名,我们将向您注册的邮箱发送密码重置链接
+              </Typography>
+              
+              <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
+                <TextField
+                  margin="normal"
+                  required
+                  fullWidth
+                  id="username"
+                  label="用户名"
+                  name="username"
+                  autoComplete="username"
+                  autoFocus
+                  value={username}
+                  onChange={(e) => setUsername(e.target.value)}
+                  helperText="请输入您注册时使用的用户名"
+                />
+                <Button
+                  type="submit"
+                  fullWidth
+                  variant="contained"
+                  sx={{ mt: 3, mb: 2 }}
+                  disabled={loading}
+                  startIcon={loading ? <CircularProgress size={20} /> : <Email />}
+                >
+                  {loading ? '发送中...' : '发送重置邮件'}
+                </Button>
+                
+                <Box sx={{ mt: 2, textAlign: 'center' }}>
+                  <Link
+                    component="button"
+                    variant="body2"
+                    onClick={handleBackToLogin}
+                    sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
+                  >
+                    <ArrowBack sx={{ fontSize: 16, mr: 0.5 }} />
+                    返回登录
+                  </Link>
+                </Box>
+              </Box>
+            </>
+          ) : (
+            <>
+              <Typography variant="body1" color="success.main" sx={{ mb: 2, textAlign: 'center' }}>
+                ✓ 密码重置邮件已发送
+              </Typography>
+              <Typography variant="body2" color="text.secondary" sx={{ mb: 3, textAlign: 'center' }}>
+                请检查您的邮箱,点击邮件中的链接重置密码。
+                <br />
+                如果未收到邮件,请检查垃圾邮件箱或稍后重试。
+              </Typography>
+              
+              <Button
+                fullWidth
+                variant="outlined"
+                onClick={handleBackToLogin}
+                startIcon={<ArrowBack />}
+                sx={{ mt: 2 }}
+              >
+                返回登录页面
+              </Button>
+            </>
+          )}
+        </Paper>
+      </Box>
+    </Container>
+  );
+};

+ 15 - 0
new-react-admin-ui/src/pages/LoginPage.tsx

@@ -100,6 +100,21 @@ export const LoginPage = () => {
             >
               {loading ? '登录中...' : '登录'}
             </Button>
+            
+            <Box sx={{ mt: 2, textAlign: 'center' }}>
+              <Typography 
+                variant="body2" 
+                color="primary"
+                sx={{ 
+                  cursor: 'pointer', 
+                  textDecoration: 'underline',
+                  '&:hover': { color: 'primary.dark' }
+                }}
+                onClick={() => window.location.href = '/forgot-password'}
+              >
+                忘记密码?
+              </Typography>
+            </Box>
           </Box>
           
           <Box sx={{ mt: 2, textAlign: 'center' }}>

+ 331 - 0
new-react-admin-ui/src/pages/ResetPasswordPage.tsx

@@ -0,0 +1,331 @@
+import { useState, useEffect } from 'react';
+import { useSearchParams, useNavigate } from 'react-router-dom';
+import { useNotify } from 'react-admin';
+import {
+  Box,
+  Button,
+  TextField,
+  Typography,
+  Container,
+  Paper,
+  Link,
+  CircularProgress,
+  Alert,
+} from '@mui/material';
+import { ArrowBack, LockReset } from '@mui/icons-material';
+
+// 密码重置API调用函数
+const validatePasswordResetToken = async (token: string) => {
+  const response = await fetch('/system/password/validate', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({ token }),
+  });
+  
+  if (!response.ok) {
+    throw new Error('验证失败');
+  }
+  
+  return response.json();
+};
+
+const resetPassword = async (token: string, newPassword: string, confirmPassword: string) => {
+  const response = await fetch('/system/password/reset', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({ token, newPassword, confirmPassword }),
+  });
+  
+  if (!response.ok) {
+    throw new Error('重置失败');
+  }
+  
+  return response.json();
+};
+
+export const ResetPasswordPage = () => {
+  const [searchParams] = useSearchParams();
+  const navigate = useNavigate();
+  const [token, setToken] = useState('');
+  const [newPassword, setNewPassword] = useState('');
+  const [confirmPassword, setConfirmPassword] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [validating, setValidating] = useState(true);
+  const [tokenValid, setTokenValid] = useState(false);
+  const [tokenError, setTokenError] = useState('');
+  const [submitted, setSubmitted] = useState(false);
+  const notify = useNotify();
+
+  useEffect(() => {
+    const tokenFromUrl = searchParams.get('token');
+    if (tokenFromUrl) {
+      setToken(tokenFromUrl);
+      validateToken(tokenFromUrl);
+    } else {
+      setValidating(false);
+      setTokenValid(false);
+      setTokenError('无效的重置链接');
+    }
+  }, [searchParams]);
+
+  const validateToken = async (tokenToValidate: string) => {
+    try {
+      const result = await validatePasswordResetToken(tokenToValidate);
+      if (result.code === 200) {
+        setTokenValid(true);
+        setTokenError('');
+      } else {
+        setTokenValid(false);
+        setTokenError(result.msg || '令牌验证失败');
+      }
+    } catch (error) {
+      setTokenValid(false);
+      setTokenError(error instanceof Error ? error.message : '验证失败');
+    } finally {
+      setValidating(false);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    
+    if (!newPassword || !confirmPassword) {
+      notify('请输入新密码和确认密码', { type: 'warning' });
+      return;
+    }
+
+    if (newPassword !== confirmPassword) {
+      notify('两次输入的密码不一致', { type: 'warning' });
+      return;
+    }
+
+    if (newPassword.length < 6) {
+      notify('密码长度不能少于6位', { type: 'warning' });
+      return;
+    }
+
+    setLoading(true);
+    
+    try {
+      const result = await resetPassword(token, newPassword, confirmPassword);
+      if (result.code === 200) {
+        setSubmitted(true);
+        notify(result.msg || '密码重置成功', { type: 'success' });
+        // 3秒后自动跳转到登录页面
+        setTimeout(() => {
+          navigate('/login');
+        }, 3000);
+      } else {
+        notify(result.msg || '密码重置失败', { type: 'error' });
+      }
+    } catch (error) {
+      notify(error instanceof Error ? error.message : '重置失败,请稍后重试', { type: 'error' });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleBackToLogin = () => {
+    navigate('/login');
+  };
+
+  if (validating) {
+    return (
+      <Container component="main" maxWidth="sm">
+        <Box
+          sx={{
+            marginTop: 8,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+          }}
+        >
+          <Paper
+            elevation={3}
+            sx={{
+              padding: 4,
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              width: '100%',
+            }}
+          >
+            <CircularProgress sx={{ mb: 2 }} />
+            <Typography variant="h6">验证重置链接...</Typography>
+          </Paper>
+        </Box>
+      </Container>
+    );
+  }
+
+  if (!tokenValid) {
+    return (
+      <Container component="main" maxWidth="sm">
+        <Box
+          sx={{
+            marginTop: 8,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+          }}
+        >
+          <Paper
+            elevation={3}
+            sx={{
+              padding: 4,
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              width: '100%',
+            }}
+          >
+            <Alert severity="error" sx={{ width: '100%', mb: 2 }}>
+              {tokenError || '重置链接无效或已过期'}
+            </Alert>
+            <Typography variant="body2" color="text.secondary" sx={{ mb: 3, textAlign: 'center' }}>
+              请重新请求密码重置或联系系统管理员
+            </Typography>
+            
+            <Button
+              fullWidth
+              variant="outlined"
+              onClick={handleBackToLogin}
+              startIcon={<ArrowBack />}
+            >
+              返回登录页面
+            </Button>
+          </Paper>
+        </Box>
+      </Container>
+    );
+  }
+
+  if (submitted) {
+    return (
+      <Container component="main" maxWidth="sm">
+        <Box
+          sx={{
+            marginTop: 8,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+          }}
+        >
+          <Paper
+            elevation={3}
+            sx={{
+              padding: 4,
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              width: '100%',
+            }}
+          >
+            <Alert severity="success" sx={{ width: '100%', mb: 2 }}>
+              ✓ 密码重置成功
+            </Alert>
+            <Typography variant="body2" color="text.secondary" sx={{ mb: 3, textAlign: 'center' }}>
+              密码已成功重置,3秒后将自动跳转到登录页面
+            </Typography>
+            
+            <Button
+              fullWidth
+              variant="outlined"
+              onClick={handleBackToLogin}
+              startIcon={<ArrowBack />}
+            >
+              立即登录
+            </Button>
+          </Paper>
+        </Box>
+      </Container>
+    );
+  }
+
+  return (
+    <Container component="main" maxWidth="sm">
+      <Box
+        sx={{
+          marginTop: 8,
+          display: 'flex',
+          flexDirection: 'column',
+          alignItems: 'center',
+        }}
+      >
+        <Paper
+          elevation={3}
+          sx={{
+            padding: 4,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+            width: '100%',
+          }}
+        >
+          <LockReset sx={{ fontSize: 40, mb: 2, color: 'primary.main' }} />
+          <Typography component="h1" variant="h4" gutterBottom>
+            重置密码
+          </Typography>
+          
+          <Typography variant="body2" color="text.secondary" sx={{ mb: 3, textAlign: 'center' }}>
+            请输入您的新密码
+          </Typography>
+          
+          <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
+            <TextField
+              margin="normal"
+              required
+              fullWidth
+              name="newPassword"
+              label="新密码"
+              type="password"
+              id="newPassword"
+              value={newPassword}
+              onChange={(e) => setNewPassword(e.target.value)}
+              helperText="密码长度不能少于6位"
+            />
+            <TextField
+              margin="normal"
+              required
+              fullWidth
+              name="confirmPassword"
+              label="确认密码"
+              type="password"
+              id="confirmPassword"
+              value={confirmPassword}
+              onChange={(e) => setConfirmPassword(e.target.value)}
+              helperText="请再次输入新密码"
+            />
+            
+            <Button
+              type="submit"
+              fullWidth
+              variant="contained"
+              sx={{ mt: 3, mb: 2 }}
+              disabled={loading}
+              startIcon={loading ? <CircularProgress size={20} /> : <LockReset />}
+            >
+              {loading ? '重置中...' : '重置密码'}
+            </Button>
+            
+            <Box sx={{ mt: 2, textAlign: 'center' }}>
+              <Link
+                component="button"
+                variant="body2"
+                onClick={handleBackToLogin}
+                sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
+              >
+                <ArrowBack sx={{ fontSize: 16, mr: 0.5 }} />
+                返回登录
+              </Link>
+            </Box>
+          </Box>
+        </Paper>
+      </Box>
+    </Container>
+  );
+};

+ 4 - 11
new-react-admin-ui/vite.config.ts

@@ -19,21 +19,14 @@ export default defineConfig({
     port: 3000,
     open: true,
     proxy: {
-      '/api': {
+      '/system': {
         target: 'http://localhost:8080',
-        changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/api/, '')
+        changeOrigin: true
       },
-      // 避免其他路径被错误代理到后端
-      '^/(?!api)': {
+      '/api': {
         target: 'http://localhost:8080',
         changeOrigin: true,
-        bypass: (req) => {
-          // 如果不是API请求,则跳过代理
-          if (!req.url.startsWith('/api')) {
-            return req.url;
-          }
-        }
+        rewrite: (path) => path.replace(/^\/api/, '')
       }
     }
   },

+ 15 - 0
pom.xml

@@ -207,6 +207,21 @@
                 <version>${ruoyi.version}</version>
             </dependency>
 
+            <!-- AWS SDK for SES -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>ses</artifactId>
+                <version>2.21.10</version>
+            </dependency>
+
+            <!-- Lombok -->
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>1.18.30</version>
+                <scope>provided</scope>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 

+ 12 - 0
ruoyi-admin/pom.xml

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

+ 130 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPasswordResetController.java

@@ -0,0 +1,130 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.service.ISysPasswordResetService;
+
+/**
+ * 密码重置控制器
+ * 
+ * @author ruoyi
+ */
+@Anonymous
+@RestController
+@RequestMapping("/system/password")
+public class SysPasswordResetController extends BaseController
+{
+    @Autowired
+    private ISysPasswordResetService passwordResetService;
+
+    /**
+     * 请求密码重置
+     */
+    @Log(title = "密码重置", businessType = BusinessType.UPDATE)
+    @PostMapping("/request")
+    public AjaxResult requestPasswordReset(@RequestBody PasswordResetRequest request)
+    {
+        return passwordResetService.requestPasswordReset(request.getUsername());
+    }
+
+    /**
+     * 验证密码重置token
+     */
+    @Log(title = "密码重置", businessType = BusinessType.UPDATE)
+    @PostMapping("/validate")
+    public AjaxResult validatePasswordResetToken(@RequestBody TokenValidationRequest request)
+    {
+        return passwordResetService.validatePasswordResetToken(request.getToken());
+    }
+
+    /**
+     * 重置密码
+     */
+    @Log(title = "密码重置", businessType = BusinessType.UPDATE)
+    @PostMapping("/reset")
+    public AjaxResult resetPassword(@RequestBody PasswordResetRequest request)
+    {
+        return passwordResetService.resetPassword(
+            request.getToken(), 
+            request.getNewPassword(), 
+            request.getConfirmPassword()
+        );
+    }
+
+    /**
+     * 清理过期的密码重置token
+     */
+    @Log(title = "密码重置", businessType = BusinessType.CLEAN)
+    @PostMapping("/clean")
+    public AjaxResult cleanExpiredTokens()
+    {
+        return passwordResetService.cleanExpiredTokens();
+    }
+
+    /**
+     * 密码重置请求参数
+     */
+    public static class PasswordResetRequest
+    {
+        private String username;
+        private String token;
+        private String newPassword;
+        private String confirmPassword;
+
+        public String getUsername() {
+            return username;
+        }
+
+        public void setUsername(String username) {
+            this.username = username;
+        }
+
+        public String getToken() {
+            return token;
+        }
+
+        public void setToken(String token) {
+            this.token = token;
+        }
+
+        public String getNewPassword() {
+            return newPassword;
+        }
+
+        public void setNewPassword(String newPassword) {
+            this.newPassword = newPassword;
+        }
+
+        public String getConfirmPassword() {
+            return confirmPassword;
+        }
+
+        public void setConfirmPassword(String confirmPassword) {
+            this.confirmPassword = confirmPassword;
+        }
+    }
+
+    /**
+     * Token验证请求参数
+     */
+    public static class TokenValidationRequest
+    {
+        private String token;
+
+        public String getToken() {
+            return token;
+        }
+
+        public void setToken(String token) {
+            this.token = token;
+        }
+    }
+}

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

@@ -0,0 +1,156 @@
+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());
+        }
+    }
+}

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

@@ -0,0 +1,100 @@
+package com.ruoyi.web.core.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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.ses.SesClient;
+
+/**
+ * AWS SES配置类
+ * 
+ * @author ruoyi
+ */
+@Configuration
+@ConfigurationProperties(prefix = "aws.ses")
+public class AwsSesConfig {
+    
+    /**
+     * AWS区域
+     */
+    private String region;
+    
+    /**
+     * AWS访问密钥ID
+     */
+    private String accessKeyId;
+    
+    /**
+     * AWS秘密访问密钥
+     */
+    private String secretAccessKey;
+    
+    /**
+     * 发件人邮箱
+     */
+    private String fromEmail;
+    
+    /**
+     * 邮件编码
+     */
+    private String charset = "UTF-8";
+    
+    /**
+     * 创建SES客户端
+     * 
+     * @return SesClient实例
+     */
+    @Bean
+    public SesClient sesClient() {
+        AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
+        
+        return SesClient.builder()
+                .region(Region.of(region))
+                .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
+                .build();
+    }
+    
+    // Getter和Setter方法
+    public String getRegion() {
+        return region;
+    }
+    
+    public void setRegion(String region) {
+        this.region = region;
+    }
+    
+    public String getAccessKeyId() {
+        return accessKeyId;
+    }
+    
+    public void setAccessKeyId(String accessKeyId) {
+        this.accessKeyId = accessKeyId;
+    }
+    
+    public String getSecretAccessKey() {
+        return secretAccessKey;
+    }
+    
+    public void setSecretAccessKey(String secretAccessKey) {
+        this.secretAccessKey = secretAccessKey;
+    }
+    
+    public String getFromEmail() {
+        return fromEmail;
+    }
+    
+    public void setFromEmail(String fromEmail) {
+        this.fromEmail = fromEmail;
+    }
+    
+    public String getCharset() {
+        return charset;
+    }
+    
+    public void setCharset(String charset) {
+        this.charset = charset;
+    }
+}

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

@@ -0,0 +1,106 @@
+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;
+        }
+    }
+    
+
+}

+ 14 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -100,6 +100,20 @@ token:
   # 令牌有效期(默认30分钟)
   expireTime: 30
 
+# AWS SES邮件配置
+aws:
+  ses:
+    # AWS区域 (例如: us-east-1, ap-southeast-1)
+    region: us-east-1
+    # AWS访问密钥ID
+    access-key-id: your-access-key-id
+    # AWS秘密访问密钥
+    secret-access-key: your-secret-access-key
+    # 发件人邮箱 (需要在AWS SES中验证过的邮箱)
+    from-email: noreply@yourdomain.com
+    # 邮件编码
+    charset: UTF-8
+
 # MyBatis配置
 mybatis:
   # 搜索指定包别名

+ 6 - 0
ruoyi-common/pom.xml

@@ -125,6 +125,12 @@
             <artifactId>jakarta.servlet-api</artifactId>
         </dependency>
 
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

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

@@ -0,0 +1,31 @@
+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);
+    
+
+}

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

@@ -0,0 +1,141 @@
+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);
+    }
+}

+ 6 - 0
ruoyi-framework/pom.xml

@@ -59,6 +59,12 @@
             <artifactId>ruoyi-system</artifactId>
         </dependency>
 
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 90 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPasswordResetToken.java

@@ -0,0 +1,90 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 密码重置token表 sys_password_reset_token
+ * 
+ * @author ruoyi
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class SysPasswordResetToken extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** token ID */
+    @Excel(name = "token ID", cellType = ColumnType.NUMERIC)
+    private Long tokenId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID", cellType = ColumnType.NUMERIC)
+    private Long userId;
+
+    /** 重置令牌 */
+    @Excel(name = "重置令牌")
+    private String token;
+
+    /** 过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date expireTime;
+
+    /** 是否已使用(0未使用 1已使用) */
+    @Excel(name = "是否已使用", readConverterExp = "0=未使用,1=已使用")
+    private Integer used;
+
+    public Long getTokenId()
+    {
+        return tokenId;
+    }
+
+    public void setTokenId(Long tokenId)
+    {
+        this.tokenId = tokenId;
+    }
+
+    public Long getUserId()
+    {
+        return userId;
+    }
+
+    public void setUserId(Long userId)
+    {
+        this.userId = userId;
+    }
+
+    public String getToken()
+    {
+        return token;
+    }
+
+    public void setToken(String token)
+    {
+        this.token = token;
+    }
+
+    public Date getExpireTime()
+    {
+        return expireTime;
+    }
+
+    public void setExpireTime(Date expireTime)
+    {
+        this.expireTime = expireTime;
+    }
+
+    public Integer getUsed()
+    {
+        return used;
+    }
+
+    public void setUsed(Integer used)
+    {
+        this.used = used;
+    }
+}

+ 60 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPasswordResetTokenMapper.java

@@ -0,0 +1,60 @@
+package com.ruoyi.system.mapper;
+
+import java.util.Date;
+import java.util.List;
+import com.ruoyi.system.domain.SysPasswordResetToken;
+
+/**
+ * 密码重置token 数据层
+ * 
+ * @author ruoyi
+ */
+public interface SysPasswordResetTokenMapper
+{
+    /**
+     * 新增密码重置token
+     * 
+     * @param passwordResetToken token对象
+     */
+    public void insertPasswordResetToken(SysPasswordResetToken passwordResetToken);
+
+    /**
+     * 根据token查询密码重置记录
+     * 
+     * @param token 重置令牌
+     * @return token对象
+     */
+    public SysPasswordResetToken selectPasswordResetTokenByToken(String token);
+
+    /**
+     * 根据用户ID查询有效的密码重置token
+     * 
+     * @param userId 用户ID
+     * @return token对象列表
+     */
+    public List<SysPasswordResetToken> selectValidPasswordResetTokenByUserId(Long userId);
+
+    /**
+     * 更新密码重置token状态
+     * 
+     * @param passwordResetToken token对象
+     * @return 结果
+     */
+    public int updatePasswordResetToken(SysPasswordResetToken passwordResetToken);
+
+    /**
+     * 删除过期的密码重置token
+     * 
+     * @param expireTime 过期时间
+     * @return 结果
+     */
+    public int deleteExpiredPasswordResetToken(Date expireTime);
+
+    /**
+     * 根据用户ID删除所有密码重置token
+     * 
+     * @param userId 用户ID
+     * @return 结果
+     */
+    public int deletePasswordResetTokenByUserId(Long userId);
+}

+ 44 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPasswordResetService.java

@@ -0,0 +1,44 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+
+/**
+ * 密码重置服务接口
+ * 
+ * @author ruoyi
+ */
+public interface ISysPasswordResetService
+{
+    /**
+     * 请求密码重置
+     * 
+     * @param username 用户名
+     * @return 结果
+     */
+    public AjaxResult requestPasswordReset(String username);
+
+    /**
+     * 验证密码重置token
+     * 
+     * @param token 重置令牌
+     * @return 结果
+     */
+    public AjaxResult validatePasswordResetToken(String token);
+
+    /**
+     * 重置密码
+     * 
+     * @param token 重置令牌
+     * @param newPassword 新密码
+     * @param confirmPassword 确认密码
+     * @return 结果
+     */
+    public AjaxResult resetPassword(String token, String newPassword, String confirmPassword);
+
+    /**
+     * 清理过期的密码重置token
+     * 
+     * @return 结果
+     */
+    public AjaxResult cleanExpiredTokens();
+}

+ 205 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPasswordResetServiceImpl.java

@@ -0,0 +1,205 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.constant.Constants;
+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.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;
+import com.ruoyi.system.mapper.SysPasswordResetTokenMapper;
+import com.ruoyi.system.service.ISysPasswordResetService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 密码重置服务实现
+ * 
+ * @author ruoyi
+ */
+@Service
+public class SysPasswordResetServiceImpl implements ISysPasswordResetService
+{
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private SysPasswordResetTokenMapper passwordResetTokenMapper;
+
+    @Autowired
+    private AppCache appCache;
+
+    @Value("${server.servlet.context-path:/}")
+    private String contextPath;
+
+    /**
+     * 密码重置token过期时间(小时)
+     */
+    @Value("${ruoyi.password.reset.token.expire:24}")
+    private int tokenExpireHours;
+
+    /**
+     * 请求密码重置
+     */
+    @Override
+    public AjaxResult requestPasswordReset(String username)
+    {
+        if (StringUtils.isEmpty(username))
+        {
+            return AjaxResult.error("用户名不能为空");
+        }
+
+        // 查询用户信息
+        SysUser user = userService.selectUserByUserName(username);
+        if (user == null)
+        {
+            return AjaxResult.error("用户不存在");
+        }
+
+        if (StringUtils.isEmpty(user.getEmail()))
+        {
+            return AjaxResult.error("用户未设置邮箱,无法发送重置邮件");
+        }
+
+        // 生成token
+        String token = generateToken();
+        Date expireTime = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(tokenExpireHours));
+
+        // 创建token记录
+        SysPasswordResetToken resetToken = new SysPasswordResetToken();
+        resetToken.setUserId(user.getUserId());
+        resetToken.setToken(token);
+        resetToken.setExpireTime(expireTime);
+        resetToken.setUsed(0);
+
+        // 保存token
+        passwordResetTokenMapper.insertPasswordResetToken(resetToken);
+
+        // 构建重置链接
+        String resetUrl = buildResetUrl(token);
+
+        // 发送重置邮件
+        boolean sendResult = MailUtils.sendPasswordResetEmail(user.getEmail(), user.getUserName(), resetUrl);
+        
+        if (sendResult)
+        {
+            return AjaxResult.success("密码重置邮件已发送到您的邮箱,请查收");
+        }
+        else
+        {
+            return AjaxResult.error("邮件发送失败,请稍后重试");
+        }
+    }
+
+    /**
+     * 验证密码重置token
+     */
+    @Override
+    public AjaxResult validatePasswordResetToken(String token)
+    {
+        if (StringUtils.isEmpty(token))
+        {
+            return AjaxResult.error("重置令牌不能为空");
+        }
+
+        SysPasswordResetToken resetToken = passwordResetTokenMapper.selectPasswordResetTokenByToken(token);
+        if (resetToken == null)
+        {
+            return AjaxResult.error("重置令牌无效");
+        }
+
+        if (resetToken.getUsed() == 1)
+        {
+            return AjaxResult.error("重置令牌已使用");
+        }
+
+        if (resetToken.getExpireTime().before(new Date()))
+        {
+            return AjaxResult.error("重置令牌已过期");
+        }
+
+        return AjaxResult.success("令牌验证成功");
+    }
+
+    /**
+     * 重置密码
+     */
+    @Override
+    public AjaxResult resetPassword(String token, String newPassword, String confirmPassword)
+    {
+        if (StringUtils.isEmpty(token))
+        {
+            return AjaxResult.error("重置令牌不能为空");
+        }
+
+        if (StringUtils.isEmpty(newPassword))
+        {
+            return AjaxResult.error("新密码不能为空");
+        }
+
+        if (!newPassword.equals(confirmPassword))
+        {
+            return AjaxResult.error("两次输入的密码不一致");
+        }
+
+        // 验证token
+        AjaxResult validateResult = validatePasswordResetToken(token);
+        if (validateResult.get("code").equals(Constants.FAIL))
+        {
+            return validateResult;
+        }
+
+        SysPasswordResetToken resetToken = passwordResetTokenMapper.selectPasswordResetTokenByToken(token);
+        
+        // 更新用户密码
+        SysUser user = new SysUser();
+        user.setUserId(resetToken.getUserId());
+        user.setPassword(SecurityUtils.encryptPassword(newPassword));
+        userService.updateUser(user);
+
+        // 标记token为已使用
+        resetToken.setUsed(1);
+        resetToken.setUpdateTime(new Date());
+        passwordResetTokenMapper.updatePasswordResetToken(resetToken);
+
+        return AjaxResult.success("密码重置成功");
+    }
+
+    /**
+     * 清理过期的密码重置token
+     */
+    @Override
+    public AjaxResult cleanExpiredTokens()
+    {
+        int deletedCount = passwordResetTokenMapper.deleteExpiredPasswordResetToken(new Date());
+        return AjaxResult.success("清理完成,共删除" + deletedCount + "个过期token");
+    }
+
+    /**
+     * 生成随机token
+     */
+    private String generateToken()
+    {
+        return UUID.randomUUID().toString().replace("-", "");
+    }
+
+    /**
+     * 构建重置链接
+     */
+    private String buildResetUrl(String token)
+    {
+        // 这里需要根据实际的前端路由来构建URL
+        // 假设前端密码重置页面路由为 /reset-password
+        String baseUrl = "http://localhost:3000"; // 实际部署时需要配置正确的域名
+        return baseUrl + contextPath + "/reset-password?token=" + token;
+    }
+}

+ 67 - 0
ruoyi-system/src/main/resources/mapper/system/SysPasswordResetTokenMapper.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysPasswordResetTokenMapper">
+
+	<resultMap type="SysPasswordResetToken" id="SysPasswordResetTokenResult">
+		<id     property="tokenId"       column="token_id"        />
+		<result property="userId"        column="user_id"         />
+		<result property="token"         column="token"           />
+		<result property="expireTime"    column="expire_time"     />
+		<result property="used"          column="used"            />
+		<result property="createTime"    column="create_time"     />
+		<result property="updateTime"     column="update_time"      />
+	</resultMap>
+
+	<insert id="insertPasswordResetToken" parameterType="SysPasswordResetToken">
+		insert into sys_password_reset_token (
+			user_id,
+			token,
+			expire_time,
+			used,
+			create_time
+		) values (
+			#{userId},
+			#{token},
+			#{expireTime},
+			#{used},
+			now()
+		)
+	</insert>
+	
+	<select id="selectPasswordResetTokenByToken" parameterType="String" resultMap="SysPasswordResetTokenResult">
+		select token_id, user_id, token, expire_time, used, create_time, update_time
+		from sys_password_reset_token
+		where token = #{token}
+	</select>
+	
+	<select id="selectValidPasswordResetTokenByUserId" parameterType="Long" resultMap="SysPasswordResetTokenResult">
+		select token_id, user_id, token, expire_time, used, create_time, update_time
+		from sys_password_reset_token
+		where user_id = #{userId}
+		  and used = 0
+		  and expire_time > now()
+		order by create_time desc
+	</select>
+	
+	<update id="updatePasswordResetToken" parameterType="SysPasswordResetToken">
+		update sys_password_reset_token
+		<set>
+			<if test="used != null">used = #{used},</if>
+			<if test="updateTime != null">update_time = now()</if>
+		</set>
+		where token_id = #{tokenId}
+	</update>
+	
+	<delete id="deleteExpiredPasswordResetToken" parameterType="Date">
+		delete from sys_password_reset_token
+		where expire_time &lt;= #{expireTime}
+	</delete>
+	
+	<delete id="deletePasswordResetTokenByUserId" parameterType="Long">
+		delete from sys_password_reset_token
+		where user_id = #{userId}
+	</delete>
+	
+</mapper>

+ 29 - 0
sql/ry_react_postgresql.sql

@@ -200,6 +200,35 @@ comment on column sys_role_menu.role_id is '角色ID';
 comment on column sys_role_menu.menu_id is '菜单ID';
 
 -- ----------------------------
+-- 8、密码重置token表
+-- ----------------------------
+drop table if exists sys_password_reset_token cascade;
+create table sys_password_reset_token (
+  token_id           bigserial                 not null    primary key,
+  user_id            bigint          not null                   ,
+  token              varchar(64)     not null                   ,
+  expire_time        timestamp(0)    not null                   ,
+  used               smallint        default 0                  ,
+  create_time        timestamp(0)    default now()              ,
+  update_time        timestamp(0)                               ,
+  constraint fk_reset_token_user foreign key (user_id) references sys_user (user_id)
+);
+
+comment on table sys_password_reset_token is '密码重置token表';
+comment on column sys_password_reset_token.token_id is 'token ID';
+comment on column sys_password_reset_token.user_id is '用户ID';
+comment on column sys_password_reset_token.token is '重置令牌';
+comment on column sys_password_reset_token.expire_time is '过期时间';
+comment on column sys_password_reset_token.used is '是否已使用(0未使用 1已使用)';
+comment on column sys_password_reset_token.create_time is '创建时间';
+comment on column sys_password_reset_token.update_time is '更新时间';
+
+-- 创建索引
+create index idx_reset_token_token on sys_password_reset_token(token);
+create index idx_reset_token_user on sys_password_reset_token(user_id);
+create index idx_reset_token_expire on sys_password_reset_token(expire_time);
+
+-- ----------------------------
 -- 初始化-角色和菜单关联表数据
 -- ----------------------------
 insert into sys_role_menu (role_id, menu_id) values (2, 1);