Kaynağa Gözat

完善权限、日志相关代码

gengjunfang 1 ay önce
ebeveyn
işleme
5ea43e6311
21 değiştirilmiş dosya ile 993 ekleme ve 210 silme
  1. 4 0
      .gitignore
  2. 41 8
      pom.xml
  3. 3 0
      src/main/java/com/zskk/pacsonline/PacsonlineApplication.java
  4. 207 88
      src/main/java/com/zskk/pacsonline/component/aop/ControllerAop.java
  5. 31 12
      src/main/java/com/zskk/pacsonline/component/response/exception/ServiceExceptionHandler.java
  6. 15 2
      src/main/java/com/zskk/pacsonline/config/SecurityConfig.java
  7. 134 95
      src/main/java/com/zskk/pacsonline/modules/system/controller/SysUserController.java
  8. 122 0
      src/main/java/com/zskk/pacsonline/modules/system/entity/SysLog.java
  9. 48 0
      src/main/java/com/zskk/pacsonline/modules/system/entity/SysSmsCode.java
  10. 15 0
      src/main/java/com/zskk/pacsonline/modules/system/mapper/SysLogMapper.java
  11. 11 0
      src/main/java/com/zskk/pacsonline/modules/system/mapper/SysSmsCodeMapper.java
  12. 2 0
      src/main/java/com/zskk/pacsonline/modules/system/request/LoginBody.java
  13. 26 0
      src/main/java/com/zskk/pacsonline/modules/system/service/SysLogService.java
  14. 15 0
      src/main/java/com/zskk/pacsonline/modules/system/service/SysSmsCodeService.java
  15. 23 0
      src/main/java/com/zskk/pacsonline/modules/system/service/SysUserService.java
  16. 28 0
      src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysLogServiceImpl.java
  17. 97 0
      src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysSmsCodeServiceImpl.java
  18. 27 3
      src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysUserServiceImpl.java
  19. 16 1
      src/main/java/com/zskk/pacsonline/security/UserDetailsServiceImpl.java
  20. 124 0
      src/main/java/com/zskk/pacsonline/utils/IpUtils.java
  21. 4 1
      src/main/resources/application.yaml

+ 4 - 0
.gitignore

@@ -5,3 +5,7 @@
 /logs/log_error.log
 /logs/log_info.log
 /logs/log_warn.log
+/logs/debug/log-debug-2025-10-28.0.log
+/logs/error/log-error-2025-10-28.0.log
+/logs/info/log-info-2025-10-28.0.log
+/logs/warn/log-warn-2025-10-28.0.log

+ 41 - 8
pom.xml

@@ -13,7 +13,8 @@
     <groupId>com.zskk</groupId>
     <artifactId>pacsonline</artifactId>
     <version>1.0.0-SNAPSHOT</version>
-
+    <name>pacsonline</name>
+    <description>中世慷慨远程诊断平台</description>
 
     <properties>
         <java.version>11</java.version>
@@ -156,6 +157,18 @@
             <scope>runtime</scope>
         </dependency>
 
+        <!-- JAXB API - JDK 11+需要手动添加,因为Java 11移除了javax.xml.bind包 -->
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
@@ -181,13 +194,6 @@
             <version>1.9.6</version>
         </dependency>
 
-        <!-- redis -->
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-data-redis</artifactId>
-            <version>2.3.4.RELEASE</version>redis
-        </dependency>
-
         <!-- hutool 工具类 -->
         <dependency>
             <groupId>cn.hutool</groupId>
@@ -204,10 +210,37 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
             </plugin>
         </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/java</directory>
+                <includes>
+                    <include>**/*.xml</include>
+                </includes>
+            </resource>
+        </resources>
     </build>
 </project>

+ 3 - 0
src/main/java/com/zskk/pacsonline/PacsonlineApplication.java

@@ -3,10 +3,13 @@ package com.zskk.pacsonline;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 
 @SpringBootApplication
+@EnableAsync  // 启用异步支持(用于异步保存操作日志)
 public class PacsonlineApplication {
 
     public static void main(String[] args) {

+ 207 - 88
src/main/java/com/zskk/pacsonline/component/aop/ControllerAop.java

@@ -1,88 +1,207 @@
-//package com.zskk.pacsonlie.component.aop;
-//
-//
-//
-//
-//import org.apache.commons.lang3.StringUtils;
-//import org.aspectj.lang.ProceedingJoinPoint;
-//import org.aspectj.lang.annotation.Around;
-//import org.aspectj.lang.annotation.Aspect;
-//import org.aspectj.lang.reflect.MethodSignature;
-//import org.slf4j.Logger;
-//import org.slf4j.LoggerFactory;
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.web.context.request.RequestAttributes;
-//import org.springframework.web.context.request.RequestContextHolder;
-//
-//import javax.annotation.Resource;
-//import javax.servlet.http.HttpServletRequest;
-//import java.lang.reflect.Method;
-//import java.time.LocalDateTime;
-//
-//@Aspect
-//@Configuration
-//public class ControllerAop {
-//
-//	@Resource
-//	private ILogService logService;
-//
-//    @Resource
-//    private IUserService userService;
-//	private final static Logger logger = LoggerFactory.getLogger(ControllerAop.class);
-//
-//	/**
-//	 * 记录系统日志
-//	 * @param jp
-//	 * @throws Throwable
-//	 */
-//	@Around(value = "execution(public * com.rimag.insight.*.controller.*Controller.*(..))")
-//	public Object userAop(ProceedingJoinPoint jp) throws Throwable {
-//		logger.info("拦截到了" + jp.getSignature().getName() +"方法...");
-//		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
-//		HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
-//		String ipAddr = IPUtils.getIpAddr(request);
-//		long startTime = System.currentTimeMillis();
-//		Object result = jp.proceed(jp.getArgs());
-//		String uid = request.getHeader("Uid");
-//		if(StringUtils.isEmpty(uid)){
-//			return result;
-//		}
-//		Long userId = Long.valueOf(uid);
-//		UserVo userVo = userService.getUserById(userId);
-//		if(userVo!=null){
-//			long overTime = System.currentTimeMillis();
-//			long useTime = overTime - startTime;
-//			Class clazz = jp.getTarget().getClass();
-//			String targetName = clazz.getSimpleName();
-//			String methodName = jp.getSignature().getName();
-//			Class[] parameterTypes = ((MethodSignature)jp.getSignature()).getMethod().getParameterTypes();
-//			Method methdo = clazz.getMethod(methodName,parameterTypes);
-//			if (methdo.getAnnotation(SystemLogHandler.class) != null) {
-//				String logDetail = methdo.getAnnotation(SystemLogHandler.class).value();
-//				if(StringUtils.isNotBlank(logDetail)){
-//					Log log=new Log();
-//					if(logDetail.indexOf("|")>0){
-//						String[] split = logDetail.split("\\|");
-//						log.setDetail(split[0]);
-//						log.setOperateType(split[1]);
-//					}else{
-//						log.setDetail(logDetail);
-//						log.setLogType("--");
-//					}
-//					log.setUserId(userId.toString());
-//					log.setUserName(userVo.getUser().getUserName());
-//					log.setRealName(userVo.getUser().getRealName());
-//					log.setTime(useTime);
-//					log.setController(targetName);
-//					log.setMethod(methodName);
-//					log.setLogType("操作日志");
-//					log.setOperateIp(ipAddr);
-//					log.setOperateTime(LocalDateTime.now());
-//					logService.addLog(log);
-//				}
-//			}
-//		}
-//		return result;
-//	}
-//
-//}
+package com.zskk.pacsonline.component.aop;
+
+import com.alibaba.fastjson.JSON;
+import com.zskk.pacsonline.modules.system.entity.SysLog;
+import com.zskk.pacsonline.modules.system.service.SysLogService;
+import com.zskk.pacsonline.security.LoginUser;
+import com.zskk.pacsonline.utils.IpUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * 控制器AOP - 记录操作日志
+ *
+ * @author admin
+ */
+@Aspect
+@Component
+public class ControllerAop {
+
+    private final static Logger logger = LoggerFactory.getLogger(ControllerAop.class);
+
+    @Autowired
+    private SysLogService sysLogService;
+
+    /**
+     * 拦截所有Controller的方法
+     * 记录系统操作日志
+     *
+     * @param jp 切点
+     * @return 方法执行结果
+     * @throws Throwable 异常
+     */
+    @Around(value = "execution(public * com.zskk.pacsonline.modules.*.controller.*Controller.*(..))")
+    public Object controllerAop(ProceedingJoinPoint jp) throws Throwable {
+        long startTime = System.currentTimeMillis();
+
+        // 获取请求对象
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (requestAttributes == null) {
+            return jp.proceed(jp.getArgs());
+        }
+
+        HttpServletRequest request = (HttpServletRequest) requestAttributes
+                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
+
+        // 执行方法
+        Object result = null;
+        Throwable exception = null;
+        try {
+            result = jp.proceed(jp.getArgs());
+        } catch (Throwable e) {
+            exception = e;
+            throw e;
+        } finally {
+            // 记录日志
+            try {
+                saveLog(jp, request, startTime, exception);
+            } catch (Exception e) {
+                logger.error("保存操作日志失败", e);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 保存操作日志
+     *
+     * @param jp        切点
+     * @param request   请求对象
+     * @param startTime 开始时间
+     * @param exception 异常对象
+     */
+    private void saveLog(ProceedingJoinPoint jp, HttpServletRequest request,
+                        long startTime, Throwable exception) {
+
+        // 获取方法签名
+        MethodSignature signature = (MethodSignature) jp.getSignature();
+        Method method = signature.getMethod();
+
+        // 检查是否有@SystemLogHandler注解
+        SystemLogHandler annotation = method.getAnnotation(SystemLogHandler.class);
+        if (annotation == null) {
+            return; // 没有注解,不记录日志
+        }
+
+        // 计算执行时间
+        long endTime = System.currentTimeMillis();
+        long useTime = endTime - startTime;
+
+        // 创建日志对象
+        SysLog log = new SysLog();
+
+        // 获取当前登录用户信息
+        try {
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {
+                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+                log.setUserId(loginUser.getUser().getId());
+                log.setUsername(loginUser.getUser().getUsername());
+                log.setRealName(loginUser.getUser().getNickname());
+            }
+        } catch (Exception e) {
+            logger.warn("获取当前登录用户失败", e);
+        }
+
+        // 解析注解值:格式为 "操作描述|操作类型"
+        String annotationValue = annotation.value();
+        if (StringUtils.isNotBlank(annotationValue)) {
+            if (annotationValue.contains("|")) {
+                String[] parts = annotationValue.split("\\|");
+                log.setDetail(parts[0]);
+                log.setOperateType(parts.length > 1 ? parts[1] : null);
+
+                // 判断是否为登录/登出操作
+                String operateType = parts.length > 1 ? parts[1] : "";
+                if ("登录".equals(operateType) || "登出".equals(operateType)) {
+                    log.setLogType("登录日志");
+                } else {
+                    log.setLogType("操作日志");
+                }
+            } else {
+                log.setDetail(annotationValue);
+                log.setLogType("操作日志");
+            }
+        }
+
+        // 控制器和方法信息
+        log.setController(jp.getTarget().getClass().getSimpleName());
+        log.setMethod(signature.getName());
+
+        // 请求信息
+        log.setRequestUrl(request.getRequestURI());
+        log.setRequestMethod(request.getMethod());
+
+        // 请求参数(过滤敏感信息)
+        Object[] args = jp.getArgs();
+        if (args != null && args.length > 0) {
+            String params = Arrays.stream(args)
+                    .filter(arg -> !(arg instanceof HttpServletRequest)
+                            && !(arg instanceof org.springframework.ui.Model)
+                            && !(arg instanceof javax.servlet.http.HttpServletResponse))
+                    .map(arg -> {
+                        try {
+                            String json = JSON.toJSONString(arg);
+                            // 过滤密码字段
+                            if (json.contains("password")) {
+                                json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"******\"");
+                            }
+                            return json;
+                        } catch (Exception e) {
+                            return arg.toString();
+                        }
+                    })
+                    .collect(Collectors.joining(", "));
+
+            // 限制参数长度
+            if (params.length() > 2000) {
+                params = params.substring(0, 2000) + "...";
+            }
+            log.setRequestParams(params);
+        }
+
+        // IP和浏览器信息
+        log.setOperateIp(IpUtils.getIpAddr(request));
+        log.setBrowser(IpUtils.getBrowser(request));
+        log.setOs(IpUtils.getOs(request));
+
+        // 操作时间和耗时
+        log.setOperateTime(LocalDateTime.now());
+        log.setUseTime(useTime);
+
+        // 状态和错误信息
+        if (exception != null) {
+            log.setStatus(0);
+            String errorMsg = exception.getMessage();
+            if (errorMsg != null && errorMsg.length() > 2000) {
+                errorMsg = errorMsg.substring(0, 2000);
+            }
+            log.setErrorMsg(errorMsg);
+        } else {
+            log.setStatus(1);
+        }
+
+        // 异步保存日志(不影响主业务)
+        sysLogService.saveLogAsync(log);
+
+        logger.info("操作日志: {} - {} - {}ms", log.getDetail(), log.getUsername(), useTime);
+    }
+}

+ 31 - 12
src/main/java/com/zskk/pacsonline/component/response/exception/ServiceExceptionHandler.java

@@ -2,6 +2,9 @@ package com.zskk.pacsonline.component.response.exception;
 
 
 import com.zskk.pacsonline.component.response.RestResult;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.ResponseBody;
@@ -27,24 +30,40 @@ public class ServiceExceptionHandler {
         return ResponseUtils.error(se.getCode(), se.getMessage());
     }
 
+    /**
+     * 处理Spring Security认证异常
+     * 注意:必须在Exception之前处理,否则会被Exception捕获
+     * AuthenticationException包括BadCredentialsException、UsernameNotFoundException等
+     */
+    @ExceptionHandler(AuthenticationException.class)
+    @ResponseBody
+    public RestResult<?> handle(AuthenticationException ex) {
+        ex.printStackTrace();
+        // 返回统一的认证失败消息,不暴露具体的错误信息(安全考虑)
+        return RestResult.error("用户名或密码错误");
+    }
+
+    /**
+     * 处理Spring Security访问拒绝异常
+     * 全局异常处理会比accessDeniedHandler先捕获accessDeniedException
+     * 这里直接返回403错误信息
+     */
+    @ExceptionHandler(AccessDeniedException.class)
+    @ResponseBody
+    public RestResult<?> handle(AccessDeniedException ex) {
+        ex.printStackTrace();
+        return RestResult.error("没有访问权限");
+    }
+
     /**
      * Exception 走的这个方法
+     * 注意:这个要放在最后,因为它会捕获所有Exception
+     * Spring Security的异常必须在此之前单独处理
      */
     @ExceptionHandler(Exception.class)
+    @ResponseBody
     public RestResult<?> handle(Exception ex) {
         ex.printStackTrace();
         return RestResult.error("请求异常, 请稍后重试!" + ex.getMessage());
     }
-
-    ///**
-    // * 全局异常处理会比accessDeniedHandler先捕获accessDeniedException
-    // * 所以这里直接抛出,交由CustomAccessDeniedHandler处理
-    // *
-    // * @param ex
-    // * @return
-    // */
-    //@ExceptionHandler(AccessDeniedException.class)
-    //public Result<?> handle(AccessDeniedException ex) {
-    //    throw ex;
-    //}
 }

+ 15 - 2
src/main/java/com/zskk/pacsonline/config/SecurityConfig.java

@@ -71,8 +71,18 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                 // 过滤请求
                 .authorizeRequests()
-                // 登录接口允许匿名访问
-                .antMatchers("/auth/login", "/auth/logout").anonymous()
+                // 登录和登出接口允许所有人访问(无论是否认证)
+                // 注意:这里必须使用permitAll()而不是anonymous()
+                // anonymous():只允许匿名用户访问,已认证用户会被拒绝
+                // permitAll():允许所有人访问,无论是否认证
+                // 登录接口如果使用anonymous(),会导致认证过程中被拒绝,出现"Bad credentials"错误
+                .antMatchers("/auth/login", 
+                             "/auth/logout", 
+                             "/auth/test-user", 
+                             "/auth/test-password", 
+                             "/auth/generate-password",
+                             "/auth/sendCode",
+                             "/auth/loginByPhone").permitAll()
                 // Swagger接口允许匿名访问
                 .antMatchers(
                         "/swagger-ui.html",
@@ -83,6 +93,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                         "/webjars/**",
                         "/doc.html"
                 ).permitAll()
+                // 测试接口允许所有人访问(开发调试用)
+                // 生产环境建议删除或限制访问
+                .antMatchers("/api/security-test/**").permitAll()
                 // 其他所有请求需要认证
                 .anyRequest().authenticated();
 

+ 134 - 95
src/main/java/com/zskk/pacsonline/modules/system/controller/SysUserController.java

@@ -1,9 +1,11 @@
 package com.zskk.pacsonline.modules.system.controller;
 
+import com.zskk.pacsonline.component.aop.SystemLogHandler;
 import com.zskk.pacsonline.component.response.RestResult;
 import com.zskk.pacsonline.modules.system.entity.SysUser;
 import com.zskk.pacsonline.modules.system.request.LoginBody;
 import com.zskk.pacsonline.modules.system.service.SysUserService;
+import com.zskk.pacsonline.modules.system.service.SysSmsCodeService;
 import com.zskk.pacsonline.utils.JwtUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -14,6 +16,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 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 javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
@@ -21,6 +25,8 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
+@RestController
+@RequestMapping("/auth")
 public class SysUserController {
     @Resource
     private AuthenticationManager authenticationManager;
@@ -37,44 +43,64 @@ public class SysUserController {
     @Autowired
     private RedisTemplate<String, String> redisTemplate;
 
+    @Resource
+    private SysSmsCodeService sysSmsCodeService;
+
     /**
      * 用户名密码登录
      * @param loginBody 登录信息
      * @return 登录结果
      */
+    @SystemLogHandler("用户登录|登录")
     @PostMapping("/login")
     public RestResult<?> login(@RequestBody LoginBody loginBody, HttpServletRequest request) {
-        // 验证用户名和密码
-        Authentication authentication = authenticationManager.authenticate(
-                new UsernamePasswordAuthenticationToken(loginBody.getUsername(), loginBody.getPassword())
-        );
-
-        // 设置认证信息
-        SecurityContextHolder.getContext().setAuthentication(authentication);
-
-        // 获取用户信息
-        SysUser user = sysUserService.getUserByUsername(loginBody.getUsername());
-
-        // 生成token
-        Map<String, Object> claims = new HashMap<>();
-        claims.put("username", user.getUsername());
-        claims.put("userId", user.getId());
-        String token = jwtUtils.generateToken(claims);
-
-        // 将token存储到redis
-        redisTemplate.opsForValue().set("token:" + user.getId(), token, 24, TimeUnit.HOURS);
-
-        // 返回结果
-        Map<String, Object> result = new HashMap<>();
-        result.put("token", token);
-        result.put("userInfo", user);
-        return  RestResult.ok("succes",result);
+        try {
+            System.out.println("=== 开始登录流程 ===");
+            System.out.println("用户名: " + loginBody.getUsername());
+            System.out.println("密码长度: " + (loginBody.getPassword() != null ? loginBody.getPassword().length() : 0));
+
+            // 验证用户名和密码
+            System.out.println("1. 开始调用authenticationManager.authenticate()");
+            Authentication authentication = authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(loginBody.getUsername(), loginBody.getPassword())
+            );
+            System.out.println("2. 认证成功!");
+
+            // 设置认证信息
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
+            // 获取用户信息
+            SysUser user = sysUserService.getUserByUsername(loginBody.getUsername());
+
+            // 生成token
+            Map<String, Object> claims = new HashMap<>();
+            claims.put("username", user.getUsername());
+            claims.put("userId", user.getId());
+            String token = jwtUtils.generateToken(claims);
+
+            // 将token存储到redis
+            redisTemplate.opsForValue().set("token:" + user.getId(), token, 24, TimeUnit.HOURS);
+
+            // 返回结果
+            Map<String, Object> result = new HashMap<>();
+            result.put("token", token);
+            result.put("userInfo", user);
+            System.out.println("=== 登录成功 ===");
+            return RestResult.ok("登录成功", result);
+        } catch (Exception e) {
+            System.err.println("=== 登录失败 ===");
+            System.err.println("异常类型: " + e.getClass().getName());
+            System.err.println("异常消息: " + e.getMessage());
+            e.printStackTrace();
+            throw e; // 重新抛出,让全局异常处理器处理
+        }
     }
 
     /**
      * 退出登录
      * @return 退出结果
      */
+    @SystemLogHandler("用户登出|登出")
     @PostMapping("/logout")
     public RestResult<?> logout() {
         // 获取当前用户
@@ -93,82 +119,95 @@ public class SysUserController {
         return RestResult.ok("退出成功");
     }
 
-    ///**
-    // * 手机号验证码登录
-    // * @param loginBody 登录信息
-    // * @return 登录结果
-    // */
-    //@PostMapping("/loginByPhone")
-    //public RestResult<?> loginByPhone(@RequestBody LoginBody loginBody) {
-    //    // 验证手机号
-    //    if (!StringUtils.isPhone(loginBody.getPhone())) {
-    //        return ResponseResult.fail(ResultCode.PARAM_ERROR);
-    //    }
-    //
-    //    // 验证验证码
-    //    String code = redisTemplate.opsForValue().get("sms:code:" + loginBody.getPhone());
-    //    if (code == null || !code.equals(loginBody.getCode())) {
-    //        return ResponseResult.fail(ResultCode.CAPTCHA_ERROR);
-    //    }
-    //
-    //    // 获取用户信息
-    //    SysUser user = sysUserService.getUserByPhone(loginBody.getPhone());
-    //    if (user == null) {
-    //        return ResponseResult.fail(ResultCode.USER_NOT_EXIST);
-    //    }
-    //
-    //    // 检查用户状态
-    //    if (user.getStatus() == 0) {
-    //        return ResponseResult.fail(ResultCode.FORBIDDEN);
-    //    }
-    //
-    //    // 生成token
-    //    Map<String, Object> claims = new HashMap<>();
-    //    claims.put("username", user.getUsername());
-    //    claims.put("userId", user.getUserId());
-    //    String token = jwtUtils.generateToken(user.getUserId().toString(), claims);
-    //
-    //    // 将token存储到redis
-    //    redisTemplate.opsForValue().set("token:" + user.getUserId(), token, 24, TimeUnit.HOURS);
-    //
-    //    // 删除验证码
-    //    redisTemplate.delete("sms:code:" + loginBody.getPhone());
-    //
-    //    // 返回结果
-    //    Map<String, Object> result = new HashMap<>();
-    //    result.put("token", token);
-    //    result.put("userInfo", user);
-    //    return ResponseResult.success(result);
-    //}
+    /**
+     * 生成BCrypt密码
+     * 仅用于开发调试,生产环境应删除
+     */
+    @PostMapping("/generate-password")
+    public RestResult<?> generatePassword(String password) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            String encodedPassword = passwordEncoder.encode(password);
+
+            result.put("rawPassword", password);
+            result.put("encodedPassword", encodedPassword);
+            result.put("updateSqlForAdmin", "UPDATE sys_user SET password = '" + encodedPassword + "' WHERE username = 'admin';");
+            result.put("updateSqlForUser", "UPDATE sys_user SET password = '" + encodedPassword + "' WHERE username = 'user';");
+
+            return RestResult.ok("密码生成成功", result);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return RestResult.error("生成失败: " + e.getMessage());
+        }
+    }
+
+
+
+
+    /**
+     * 手机号验证码登录
+     * @param loginBody 登录信息
+     * @return 登录结果
+     */
+    @SystemLogHandler("手机号验证码登录|登录")
+    @PostMapping("/loginByPhone")
+    public RestResult<?> loginByPhone(@RequestBody LoginBody loginBody) {
+        String phone = loginBody.getPhone();
+        String code = loginBody.getCode();
+        if (!isPhone(phone)) {
+            return RestResult.error("手机号格式不正确");
+        }
+
+        boolean ok = sysSmsCodeService.validateAndConsumeCode(phone, code);
+        if (!ok) {
+            return RestResult.error("验证码错误或已过期");
+        }
+
+        SysUser user = sysUserService.getUserByPhone(phone);
+        if (user == null) {
+            return RestResult.error("用户不存在");
+        }
+        if (user.getStatus() != null && user.getStatus() == 0) {
+            return RestResult.error("用户已被禁用");
+        }
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("username", user.getUsername());
+        claims.put("userId", user.getId());
+        String token = jwtUtils.generateToken(claims);
+
+        redisTemplate.opsForValue().set("token:" + user.getId(), token, 24, TimeUnit.HOURS);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("token", token);
+        result.put("userInfo", user);
+        return RestResult.ok("登录成功", result);
+    }
 
     ///**
     // * 发送验证码
     // * @param phone 手机号
     // * @return 发送结果
     // */
-    //@PostMapping("/sendCode")
-    //public ResponseResult<?> sendCode(String phone) {
-    //    // 验证手机号
-    //    if (!StringUtils.isPhone(phone)) {
-    //        return ResponseResult.fail(ResultCode.PARAM_ERROR);
-    //    }
-    //
-    //    // 检查手机号是否存在
-    //    if (!sysUserService.checkPhoneExist(phone)) {
-    //        return ResponseResult.fail(ResultCode.PHONE_NOT_EXIST);
-    //    }
-    //
-    //    // 生成验证码
-    //    String code = StringUtils.generateVerifyCode(6);
-    //
-    //    // 存储验证码到redis,有效期5分钟
-    //    redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES);
-    //
-    //    // TODO: 调用短信发送服务发送验证码
-    //    System.out.println("发送验证码: " + code + " 到手机号: " + phone);
-    //
-    //    return ResponseResult.success("验证码发送成功");
-    //}
+    @PostMapping("/sendCode")
+    public RestResult<?> sendCode(String phone, HttpServletRequest request) {
+        if (!isPhone(phone)) {
+            return RestResult.error("手机号格式不正确");
+        }
+        if (!sysUserService.checkPhoneExist(phone)) {
+            return RestResult.error("手机号未注册");
+        }
+        String ip = request != null ? request.getRemoteAddr() : null;
+        sysSmsCodeService.generateAndSendCode(phone, ip);
+        return RestResult.ok("验证码发送成功");
+    }
+
+    private boolean isPhone(String phone) {
+        if (phone == null) {
+            return false;
+        }
+        return phone.matches("^1[3-9]\\d{9}$");
+    }
 
 
 

+ 122 - 0
src/main/java/com/zskk/pacsonline/modules/system/entity/SysLog.java

@@ -0,0 +1,122 @@
+package com.zskk.pacsonline.modules.system.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 系统操作日志实体
+ *
+ * @author admin
+ */
+@Data
+@TableName("sys_log")
+public class SysLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 日志ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 真实姓名
+     */
+    private String realName;
+
+    /**
+     * 日志类型:登录日志、操作日志
+     */
+    private String logType;
+
+    /**
+     * 操作类型:增删改查等
+     */
+    private String operateType;
+
+    /**
+     * 日志详情描述
+     */
+    private String detail;
+
+    /**
+     * 控制器名称
+     */
+    private String controller;
+
+    /**
+     * 方法名称
+     */
+    private String method;
+
+    /**
+     * 请求URL
+     */
+    private String requestUrl;
+
+    /**
+     * 请求方式:GET/POST/PUT/DELETE
+     */
+    private String requestMethod;
+
+    /**
+     * 请求参数
+     */
+    private String requestParams;
+
+    /**
+     * 操作IP地址
+     */
+    private String operateIp;
+
+    /**
+     * 操作地点
+     */
+    private String operateLocation;
+
+    /**
+     * 操作时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime operateTime;
+
+    /**
+     * 执行时长(毫秒)
+     */
+    private Long useTime;
+
+    /**
+     * 状态:0-失败,1-成功
+     */
+    private Integer status;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 浏览器
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+}

+ 48 - 0
src/main/java/com/zskk/pacsonline/modules/system/entity/SysSmsCode.java

@@ -0,0 +1,48 @@
+package com.zskk.pacsonline.modules.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("sys_sms_code")
+public class SysSmsCode implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 手机号 */
+    private String phone;
+
+    /** 验证码 */
+    private String code;
+
+    /** 过期时间 */
+    private LocalDateTime expireTime;
+
+    /** 是否已使用:0 未使用,1 已使用 */
+    private Integer used;
+
+    /** 使用时间 */
+    private LocalDateTime usedTime;
+
+    /** 请求IP */
+    private String requestIp;
+
+    /** 发送状态:0 待发送/模拟,1 成功,2 失败 */
+    private Integer sendStatus;
+
+    /** 发送返回信息(失败原因等) */
+    private String sendMsg;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+}
+
+

+ 15 - 0
src/main/java/com/zskk/pacsonline/modules/system/mapper/SysLogMapper.java

@@ -0,0 +1,15 @@
+package com.zskk.pacsonline.modules.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zskk.pacsonline.modules.system.entity.SysLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 系统日志Mapper
+ *
+ * @author admin
+ */
+@Mapper
+public interface SysLogMapper extends BaseMapper<SysLog> {
+
+}

+ 11 - 0
src/main/java/com/zskk/pacsonline/modules/system/mapper/SysSmsCodeMapper.java

@@ -0,0 +1,11 @@
+package com.zskk.pacsonline.modules.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zskk.pacsonline.modules.system.entity.SysSmsCode;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysSmsCodeMapper extends BaseMapper<SysSmsCode> {
+}
+
+

+ 2 - 0
src/main/java/com/zskk/pacsonline/modules/system/request/LoginBody.java

@@ -6,4 +6,6 @@ import lombok.Data;
 public class LoginBody {
     String username;
     String password;
+    String phone;
+    String code;
 }

+ 26 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/SysLogService.java

@@ -0,0 +1,26 @@
+package com.zskk.pacsonline.modules.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zskk.pacsonline.modules.system.entity.SysLog;
+
+/**
+ * 系统日志服务接口
+ *
+ * @author admin
+ */
+public interface SysLogService extends IService<SysLog> {
+
+    /**
+     * 保存操作日志
+     *
+     * @param log 日志对象
+     */
+    void saveLog(SysLog log);
+
+    /**
+     * 异步保存操作日志
+     *
+     * @param log 日志对象
+     */
+    void saveLogAsync(SysLog log);
+}

+ 15 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/SysSmsCodeService.java

@@ -0,0 +1,15 @@
+package com.zskk.pacsonline.modules.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zskk.pacsonline.modules.system.entity.SysSmsCode;
+
+public interface SysSmsCodeService extends IService<SysSmsCode> {
+
+    /** 生成并发送验证码(模拟发送),同时持久化与写入缓存 */
+    void generateAndSendCode(String phone, String requestIp);
+
+    /** 校验验证码并消费(标记已使用) */
+    boolean validateAndConsumeCode(String phone, String code);
+}
+
+

+ 23 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/SysUserService.java

@@ -30,4 +30,27 @@ public interface SysUserService extends IService<SysUser> {
 
     SysUser getUserByUsername(String username);
 
+    /**
+     * 根据手机号查询用户
+     * @param phone 手机号
+     * @return 用户信息
+     */
+    SysUser getUserByPhone(String phone);
+
+    /**
+     * 检查手机号是否存在
+     * @param phone 手机号
+     * @return 是否存在
+     */
+    boolean checkPhoneExist(String phone);
+
+    /**
+     * 验证密码是否匹配
+     *
+     * @param rawPassword 原始密码
+     * @param encodedPassword 加密后的密码
+     * @return 是否匹配
+     */
+    boolean matchesPassword(String rawPassword, String encodedPassword);
+
 }

+ 28 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysLogServiceImpl.java

@@ -0,0 +1,28 @@
+package com.zskk.pacsonline.modules.system.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zskk.pacsonline.modules.system.entity.SysLog;
+import com.zskk.pacsonline.modules.system.mapper.SysLogMapper;
+import com.zskk.pacsonline.modules.system.service.SysLogService;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+/**
+ * 系统日志服务实现
+ *
+ * @author admin
+ */
+@Service
+public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> implements SysLogService {
+
+    @Override
+    public void saveLog(SysLog log) {
+        save(log);
+    }
+
+    @Override
+    @Async
+    public void saveLogAsync(SysLog log) {
+        save(log);
+    }
+}

+ 97 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysSmsCodeServiceImpl.java

@@ -0,0 +1,97 @@
+package com.zskk.pacsonline.modules.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zskk.pacsonline.modules.system.entity.SysSmsCode;
+import com.zskk.pacsonline.modules.system.mapper.SysSmsCodeMapper;
+import com.zskk.pacsonline.modules.system.service.SysSmsCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class SysSmsCodeServiceImpl extends ServiceImpl<SysSmsCodeMapper, SysSmsCode> implements SysSmsCodeService {
+
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    private static final String REDIS_KEY_PREFIX = "sms:code:";
+    private static final int EXPIRE_MINUTES = 5;
+
+    @Override
+    public void generateAndSendCode(String phone, String requestIp) {
+        String code = generateVerifyCode(6);
+
+        SysSmsCode sms = new SysSmsCode();
+        sms.setPhone(phone);
+        sms.setCode(code);
+        sms.setCreateTime(LocalDateTime.now());
+        sms.setExpireTime(LocalDateTime.now().plusMinutes(EXPIRE_MINUTES));
+        sms.setUsed(0);
+        sms.setRequestIp(requestIp);
+        sms.setSendStatus(1);
+        sms.setSendMsg("MOCK_SEND_SUCCESS");
+        save(sms);
+
+        redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + phone, code, EXPIRE_MINUTES, TimeUnit.MINUTES);
+
+        System.out.println("发送验证码: " + code + " 到手机号: " + phone);
+    }
+
+    @Override
+    public boolean validateAndConsumeCode(String phone, String code) {
+        String cacheKey = REDIS_KEY_PREFIX + phone;
+        String cached = redisTemplate.opsForValue().get(cacheKey);
+
+        boolean matched = code != null && code.equals(cached);
+        if (!matched) {
+            // 兜底到数据库最新未过期且未使用的记录
+            LambdaQueryWrapper<SysSmsCode> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(SysSmsCode::getPhone, phone)
+                    .eq(SysSmsCode::getUsed, 0)
+                    .gt(SysSmsCode::getExpireTime, LocalDateTime.now())
+                    .orderByDesc(SysSmsCode::getId)
+                    .last("limit 1");
+            SysSmsCode latest = getOne(wrapper);
+            matched = latest != null && code != null && code.equals(latest.getCode());
+            if (matched) {
+                latest.setUsed(1);
+                latest.setUsedTime(LocalDateTime.now());
+                updateById(latest);
+                redisTemplate.delete(cacheKey);
+            }
+            return matched;
+        } else {
+            // 命中缓存,标记数据库最新记录为已使用
+            LambdaQueryWrapper<SysSmsCode> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(SysSmsCode::getPhone, phone)
+                    .eq(SysSmsCode::getUsed, 0)
+                    .gt(SysSmsCode::getExpireTime, LocalDateTime.now())
+                    .orderByDesc(SysSmsCode::getId)
+                    .last("limit 1");
+            SysSmsCode latest = getOne(wrapper);
+            if (latest != null && code != null && code.equals(latest.getCode())) {
+                latest.setUsed(1);
+                latest.setUsedTime(LocalDateTime.now());
+                updateById(latest);
+            }
+            redisTemplate.delete(cacheKey);
+            return true;
+        }
+    }
+
+    private String generateVerifyCode(int length) {
+        Random random = new Random();
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append(random.nextInt(10));
+        }
+        return sb.toString();
+    }
+}
+
+

+ 27 - 3
src/main/java/com/zskk/pacsonline/modules/system/service/impl/SysUserServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zskk.pacsonline.modules.system.entity.SysUser;
 import com.zskk.pacsonline.modules.system.mapper.SysUserMapper;
 import com.zskk.pacsonline.modules.system.service.SysUserService;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -21,20 +22,43 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
     @Resource
     private SysUserMapper sysUserMapper;
 
+    @Resource
+    private PasswordEncoder passwordEncoder;
+
     @Override
     public SysUser getUserWithRolesAndPermissions(String username) {
-        return baseMapper.selectUserWithRolesAndPermissions(username);
+        return sysUserMapper.selectUserWithRolesAndPermissions(username);
     }
 
     @Override
     public List<String> getPermissionsByUserId(Long userId) {
-        return baseMapper.selectPermissionsByUserId(userId);
+        return sysUserMapper.selectPermissionsByUserId(userId);
     }
 
+    @Override
     public SysUser getUserByUsername(String username){
-        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();
+        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(SysUser::getUsername,username);
         SysUser user = getOne(queryWrapper);
         return user;
     }
+
+    @Override
+    public SysUser getUserByPhone(String phone) {
+        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(SysUser::getPhone, phone);
+        return getOne(queryWrapper);
+    }
+
+    @Override
+    public boolean checkPhoneExist(String phone) {
+        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(SysUser::getPhone, phone);
+        return count(queryWrapper) > 0;
+    }
+
+    @Override
+    public boolean matchesPassword(String rawPassword, String encodedPassword) {
+        return passwordEncoder.matches(rawPassword, encodedPassword);
+    }
 }

+ 16 - 1
src/main/java/com/zskk/pacsonline/security/UserDetailsServiceImpl.java

@@ -24,14 +24,29 @@ public class UserDetailsServiceImpl implements UserDetailsService {
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        System.out.println("=== UserDetailsServiceImpl.loadUserByUsername ===");
+        System.out.println("查询用户: " + username);
+
         SysUser user = userService.getUserWithRolesAndPermissions(username);
         if (Objects.isNull(user)) {
+            System.err.println("错误:用户不存在");
             throw new UsernameNotFoundException("用户" + username + "不存在");
         }
 
+        System.out.println("用户ID: " + user.getId());
+        System.out.println("用户名: " + user.getUsername());
+        System.out.println("状态: " + user.getStatus());
+        System.out.println("密码是否为空: " + (user.getPassword() == null));
+        System.out.println("密码前缀: " + (user.getPassword() != null ? user.getPassword().substring(0, Math.min(10, user.getPassword().length())) : "NULL"));
+
         // 获取用户权限列表
         List<String> permissions = userService.getPermissionsByUserId(user.getId());
+        System.out.println("权限数量: " + (permissions != null ? permissions.size() : 0));
+
+        LoginUser loginUser = new LoginUser(user, permissions);
+        System.out.println("创建LoginUser成功");
+        System.out.println("LoginUser.getPassword(): " + (loginUser.getPassword() != null ? loginUser.getPassword().substring(0, Math.min(10, loginUser.getPassword().length())) : "NULL"));
 
-        return new LoginUser(user, permissions);
+        return loginUser;
     }
 }

+ 124 - 0
src/main/java/com/zskk/pacsonline/utils/IpUtils.java

@@ -0,0 +1,124 @@
+package com.zskk.pacsonline.utils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IP地址工具类
+ *
+ * @author admin
+ */
+public class IpUtils {
+
+    /**
+     * 获取客户端真实IP地址
+     *
+     * @param request 请求对象
+     * @return IP地址
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        if (request == null) {
+            return "unknown";
+        }
+
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ip != null && ip.length() > 15) {
+            if (ip.indexOf(",") > 0) {
+                ip = ip.substring(0, ip.indexOf(","));
+            }
+        }
+
+        // 本地访问
+        if ("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)) {
+            ip = "127.0.0.1";
+        }
+
+        return ip;
+    }
+
+    /**
+     * 获取浏览器信息
+     *
+     * @param request 请求对象
+     * @return 浏览器信息
+     */
+    public static String getBrowser(HttpServletRequest request) {
+        if (request == null) {
+            return "unknown";
+        }
+
+        String userAgent = request.getHeader("User-Agent");
+        if (userAgent == null) {
+            return "unknown";
+        }
+
+        userAgent = userAgent.toLowerCase();
+
+        if (userAgent.contains("edge")) {
+            return "Edge";
+        } else if (userAgent.contains("chrome")) {
+            return "Chrome";
+        } else if (userAgent.contains("firefox")) {
+            return "Firefox";
+        } else if (userAgent.contains("safari")) {
+            return "Safari";
+        } else if (userAgent.contains("opera")) {
+            return "Opera";
+        } else if (userAgent.contains("msie") || userAgent.contains("trident")) {
+            return "IE";
+        } else {
+            return "unknown";
+        }
+    }
+
+    /**
+     * 获取操作系统信息
+     *
+     * @param request 请求对象
+     * @return 操作系统信息
+     */
+    public static String getOs(HttpServletRequest request) {
+        if (request == null) {
+            return "unknown";
+        }
+
+        String userAgent = request.getHeader("User-Agent");
+        if (userAgent == null) {
+            return "unknown";
+        }
+
+        userAgent = userAgent.toLowerCase();
+
+        if (userAgent.contains("windows")) {
+            return "Windows";
+        } else if (userAgent.contains("mac")) {
+            return "Mac OS";
+        } else if (userAgent.contains("linux")) {
+            return "Linux";
+        } else if (userAgent.contains("unix")) {
+            return "Unix";
+        } else if (userAgent.contains("android")) {
+            return "Android";
+        } else if (userAgent.contains("iphone") || userAgent.contains("ipad")) {
+            return "iOS";
+        } else {
+            return "unknown";
+        }
+    }
+}

+ 4 - 1
src/main/resources/application.yaml

@@ -48,10 +48,13 @@ spring:
       filters: stat
 logging:
   config: classpath:logback-spring.xml
+  level:
+    org.apache.ibatis.builder.xml: DEBUG  # 打印 XML 加载日志
+    org.springframework.security: DEBUG # 打印认证相关详细日志
 mybatis-plus:
   mapper-locations:
     - classpath:mapper/*.xml
-    - classpath*:com/**/mapper/*.xml
+    - classpath*:com/**/mapper/**/*.xml
   global-config:
     banner: false  # 关闭 MyBatis-Plus 启动图标日志(核心配置)
   # 可选:开启/关闭 SQL 日志(根据需要取消注释)