目录

一、前言

二、后端开发及调整

1.日志管理开发

2.配置调整

 3.日志入库(注解、切面)

三、前端调整

1.日志管理开发

四、附:源码

1.源码下载地址

五、结语

一、前言

此文章在上次调整的基础上开发后端管理系统的用户请求日志功能,并集成了Spring Security用来替代jwt认证和缓存用户信息,以便于日志能记录详细的用户操作信息。新增日志管理菜单可视化日志信息。

此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:权限管理(三)

(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)

二、后端开发及调整

1.日志管理开发

1.新建用户操作日志表user_log

CREATE TABLE `user_log` (`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',`type` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作类型',`method` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作接口',`status` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作状态(0成功,1失败)',`text` text COLLATE utf8mb4_general_ci COMMENT '响应结果',`ip_address` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'ip地址',`user_agent` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '客户端信息',`user_id` int DEFAULT NULL COMMENT '操作员ID(用户id)',`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间/操作时间',`create_by` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人/操作员',`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`update_by` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',`del_flag` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标识0未删除,1已删除(逻辑删除)',`remark` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=253 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户操作日志表'

2.新增用户操作日志 实体类UserLogEntity.java


import lombok.Data;/*** 用户操作日志表* @TableName user_log*/
@Data
public class UserLogEntity extends BaseEntity{/*** 主键*/private Integer id;/*** 操作类型*/private String type;/*** 操作接口*/private String method;/*** 操作状态(0成功,1失败)*/private String status;/*** 响应结果*/private String text;/*** ip地址*/private String ipAddress;/*** 客户端信息*/private String userAgent;/*** 操作员ID(用户id)*/private Integer userId;/*** 备注*/private String remark;
}

3.新增用户操作日志控制器类LogController.java


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.service.UserLogService;
import org.wal.userdemo.utils.Result;import java.util.List;/*** 日志控制器类* 处理用户日志相关的API请求*/
@RestController
@RequestMapping("/api/log")
public class LogController {@Autowiredprivate UserLogService userLogService;/*** 获取用户日志列表* 根据查询条件获取用户日志数据,支持分页功能** @param queryUserLogReq 用户日志查询请求参数对象,包含查询条件和分页信息* @return Result 返回封装的用户日志列表结果,包含数据列表和总记录数*/@PostMapping("/getUserLogList")public Result<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq) {// 查询用户日志列表数据List<UserLogEntity> userLogList = userLogService.getUserLogList(queryUserLogReq);// 获取符合条件的用户日志总记录数int total = userLogService.getUserLogCount(queryUserLogReq);return Result.page(userLogList, total);}@GetMapping("/getUserLogById")public Result<UserLogEntity> getUserLogById(Integer id) {// 返回用户日志数据return Result.success(userLogService.getUserLogById(id));}
}

 4.新增用户操作日志服务类UserLogService.java


import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;import java.util.List;public interface UserLogService {int insert(UserLogEntity record);List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq);int getUserLogCount(QueryUserLogReq queryUserLogReq);UserLogEntity getUserLogById(Integer id);
}

5.新增用户操作日志实现类UserLogServiceImpl.java


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.mapper.UserLogMapper;
import org.wal.userdemo.service.UserLogService;import java.util.Collections;
import java.util.List;@Service
public class UserLogServiceImpl implements UserLogService {@Autowiredprivate UserLogMapper userLogMapper;@Overridepublic int insert(UserLogEntity record) {return userLogMapper.insert( record);}@Overridepublic List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq) {return userLogMapper.getUserLogList(queryUserLogReq);}@Overridepublic int getUserLogCount(QueryUserLogReq queryUserLogReq) {return userLogMapper.getUserLogCount(queryUserLogReq);}@Overridepublic UserLogEntity getUserLogById(Integer id) {return userLogMapper.getUserLogById(id);}
}

6.新增用户操作日志Mapper接口类UserLogMapper.java

import org.apache.ibatis.annotations.Mapper;
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;import java.util.List;@Mapper
public interface UserLogMapper {int insert(UserLogEntity record);List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq);int getUserLogCount(QueryUserLogReq queryUserLogReq);UserLogEntity getUserLogById(Integer id);
}

6.新增用户操作日志Mapper.xml文件UserLogMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wal.userdemo.mapper.UserLogMapper"><resultMap id="BaseResultMap" type="org.wal.userdemo.entity.UserLogEntity"><id property="id" column="id" /><result property="type" column="type" /><result property="method" column="method" /><result property="status" column="status" /><result property="text" column="text" /><result property="ipAddress" column="ip_address" /><result property="userAgent" column="user_agent" /><result property="userId" column="user_id" /><result property="createTime" column="create_time" /><result property="createBy" column="create_by" /><result property="updateTime" column="update_time" /><result property="updateBy" column="update_by" /><result property="delFlag" column="del_flag" /></resultMap><sql id="Base_Column_List">id,type,method,status,text,ip_address,user_agent,user_id,create_time,create_by,update_time,update_by,del_flag</sql><insert id="insert" parameterType="org.wal.userdemo.entity.UserLogEntity" >insert into user_log<trim prefix="(" suffix=")" suffixOverrides=","><if test="type != null">type,</if><if test="method != null">method,</if><if test="status != null">status,</if><if test="text != null">text,</if><if test="ipAddress != null">ip_address,</if><if test="userAgent != null">user_agent,</if><if test="userId != null">user_id,</if><if test="createTime != null">create_time,</if><if test="createBy != null">create_by,</if><if test="updateTime != null">update_time,</if><if test="updateBy != null">update_by,</if><if test="delFlag != null">del_flag,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="type != null">#{type},</if><if test="method != null">#{method},</if><if test="status != null">#{status},</if><if test="text != null">#{text},</if><if test="ipAddress != null">#{ipAddress},</if><if test="userAgent != null">#{userAgent},</if><if test="userId != null">#{userId},</if><if test="createTime != null">#{createTime},</if><if test="createBy != null">#{createBy},</if><if test="updateTime != null">#{updateTime},</if><if test="updateBy != null">#{updateBy},</if><if test="delFlag != null">#{delFlag},</if></trim></insert><select id="getUserLogList" resultMap="BaseResultMap">select<include refid="Base_Column_List"/>from user_log<where><if test="type != null">and type = #{type}</if><if test="status != null">and status = #{status}</if><if test="createTime != null">and create_time like CONCAT('%',#{createTime},'%')</if></where></select><select id="getUserLogCount" resultType="java.lang.Integer">select count(1) from user_log<where><if test="type != null">and type = #{type}</if><if test="status != null">and status = #{status}</if><if test="createTime != null">and create_time like CONCAT('%',#{createTime},'%')</if></where></select><select id="getUserLogById" resultMap="BaseResultMap" parameterType="integer">select <include refid="Base_Column_List"/>from user_logwhere id = #{id}</select></mapper>
2.配置调整

1.新增pom.xml依赖,支持SpringSecurity

      <!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--常用工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency>

2.移除JwtInterceptor.java拦截器,改用Spring Security进行认证、在上下文缓存登录用户信息。

3.移除WebConfig.java配置的请求拦截器,改用Spring Security的config进行过滤和拦截。

4.新增LoginUser.java类,实现Spring Security的UserDetails,封装用户认证和授权信息。


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.wal.userdemo.entity.UserEntity;import java.util.Collection;
import java.util.Collections;/*** 登录用户类,实现Spring Security的UserDetails接口* 用于封装用户认证和授权信息*/
public class LoginUser implements UserDetails {private Integer userId;private String username;private String password;/*** 构造函数,根据UserEntity初始化LoginUser* @param user 用户实体对象,包含用户的基本信息*/public LoginUser(UserEntity user) {this.userId = user.getId();this.username = user.getName();this.password = user.getPassword();}/*** 获取用户ID* @return 用户ID*/public Integer getUserId() {return userId;}/*** 获取用户权限集合* @return 包含用户权限的集合,默认返回"USER"权限*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return Collections.singletonList(new SimpleGrantedAuthority("USER"));}/*** 获取用户密码* @return 用户密码*/@Overridepublic String getPassword() {return password;}/*** 获取用户名* @return 用户名*/@Overridepublic String getUsername() {return username;}// 实现其他UserDetails方法.../*** 判断账户是否未过期* @return true表示账户未过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 判断账户是否未锁定* @return true表示账户未锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 判断凭证是否未过期* @return true表示凭证未过期*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 判断账户是否启用* @return true表示账户已启用*/@Overridepublic boolean isEnabled() {return true;}
}

5. 新增JwtAuthenticationTokenFilter.java ,jwt认证过滤器,验证jwt令牌的有效性,有效则解析令牌信息中的用户信息并设置Spring Security的认证上下文。


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;
import org.wal.userdemo.utils.JwtUtil;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** JWT认证过滤器,用于拦截请求并验证JWT令牌的有效性。* 如果令牌有效,则从令牌中解析用户信息,并设置Spring Security的认证上下文。*/
@Component
//@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserMapper userMapper;/*** 执行JWT认证的核心逻辑。* 该方法会在每个HTTP请求中执行一次,检查请求头中的Authorization字段是否包含有效的JWT令牌。** @param request  HTTP请求对象* @param response HTTP响应对象* @param chain    过滤器链,用于继续执行后续过滤器* @throws ServletException 当Servlet处理出现异常时抛出* @throws IOException      当IO操作出现异常时抛出*/@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {// 获取请求头中的Authorization字段String authHeader = request.getHeader("Authorization");// 判断是否存在Bearer类型的JWT令牌if (authHeader != null && authHeader.startsWith("Bearer ")) {// 提取JWT令牌(去除Bearer前缀)String token = authHeader.substring(7);// 验证JWT令牌是否有效if (jwtUtil.validateToken(token)) {try {// 解析JWT令牌中的用户IDString userId = jwtUtil.parseUserId(token);// 根据用户ID查询用户信息UserEntity user = userMapper.getUserById(Integer.parseInt(userId));// 加载用户详细信息UserDetails userDetails = userDetailsService.loadUserByUsername(user.getName());// 创建认证对象UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 设置认证详情authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证信息存入安全上下文SecurityContextHolder.getContext().setAuthentication(authentication);} catch (Exception e) {// 记录JWT验证过程中的异常信息logger.error("JWT验证异常:", e);}}}// 继续执行过滤器链chain.doFilter(request, response);}
}

6.新增UserDetailsConfig.java类,实现Spring Security的UserDetailsService,用于加载用户详细信息进行认证


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;/*** 用户详情配置类,实现Spring Security的UserDetailsService接口* 用于加载用户详细信息进行认证*/
@Service
public class UserDetailsConfig implements UserDetailsService {@Autowiredprivate UserMapper userMapper;/*** 根据用户名加载用户详细信息* @param name 用户名* @return UserDetails 用户详细信息对象* @throws UsernameNotFoundException 当用户不存在时抛出此异常*/@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {// 通过用户名查询用户信息UserEntity user = userMapper.getUserByName(name);// 如果用户不存在,抛出用户名未找到异常if (user == null) {throw new UsernameNotFoundException("用户不存在");}// 将用户实体转换为登录用户对象并返回return new LoginUser(user);}
}

7.新增SecurityConfig.java安全配置类,配置用户认证、JWT过滤等相关安全组件。


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.wal.userdemo.Filter.JwtAuthenticationTokenFilter;/*** Spring Security安全配置类* 配置用户认证、权限控制、JWT过滤器等安全相关组件*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 创建密码编码器Bean* 使用BCrypt算法对密码进行加密处理** @return PasswordEncoder 密码编码器实例*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置认证管理器构建器* 设置自定义用户详情服务和密码编码器** @param auth AuthenticationManagerBuilder认证管理器构建器* @throws Exception 配置过程中可能抛出的异常*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 暴露AuthenticationManager作为Bean* 用于在应用其他地方进行手动认证操作** @return AuthenticationManager 认证管理器实例* @throws Exception 获取认证管理器时可能抛出的异常*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 配置HTTP安全策略* 包括CSRF禁用、会话管理、URL权限控制和JWT过滤器添加** @param http HttpSecurity HTTP安全配置构建器* @throws Exception 配置过程中可能抛出的异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 禁用CSRF保护,配置无状态会话管理http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 配置URL访问权限.authorizeRequests()// 登录接口允许所有人访问.antMatchers("/api/auth/login").permitAll()// API接口需要认证后访问.antMatchers("/api/**").authenticated()// 其他所有请求都需要认证.anyRequest().authenticated();// 在用户名密码认证过滤器之前添加JWT认证过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
}

8.重写LoginController.java的登录接口进行身份验证

 /*** 用户登录接口* 通过用户名和密码进行身份验证,验证成功后生成JWT token返回** @param request 登录请求参数对象,包含用户名和密码* @return Result<?> 返回登录结果,成功时返回JWT token,失败时返回错误信息*/@UserLog("登录接口")@PostMapping("/login")public Result<?> login(@RequestBody LoginReq request) {try {// 使用Spring Security进行用户身份验证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));// 将认证结果存储到安全上下文中SecurityContextHolder.getContext().setAuthentication(authentication);// 从认证结果中获取用户信息String username = authentication.getName();UserEntity user = userService.getUserByName(username);if (user != null) {// 生成JWT token并返回String token = jwtUtil.generateToken(user.getId());return Result.success(token);} else {return Result.error("用户不存在");}} catch (Exception e) {e.printStackTrace();return Result.error("用户名或密码错误2");}}

9.重写JwtUtil.java工具类,固定签名秘钥, 新增校验Jwt令牌方法。


import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;
/*** JwtUtil 是一个工具类,用于生成和解析 JWT(JSON Web Token)。* 主要功能包括:* - 生成带有用户名和过期时间的 JWT 令牌* - 从 JWT 令牌中解析出用户名** 注意事项:* - 密钥(SECRET_KEY)应通过配置文件管理,避免硬编码* - 过期时间(EXPIRATION)可按业务需求调整*/
@Component
public class JwtUtil {/*** JWT 签名所使用的密钥。* 在生产环境中建议使用更安全的方式存储,如配置中心或环境变量。*/private static final String SECRET = "myFixedSecretKey12345678901234567890";public static final Key SIGNING_KEY = Keys.hmacShaKeyFor(SECRET.getBytes());/*** JWT 令牌的有效期,单位为毫秒。* 当前设置为 24 小时(86,400,000 毫秒)。*/private static final long EXPIRATION = 86400000; // 24小时/*** 生成 JWT 令牌。** @param userId 用户id,作为 JWT 的 subject 字段* @return 返回生成的 JWT 字符串*/public static String generateToken(Integer userId) {return Jwts.builder()// 设置 JWT 的主题(通常为用户标识).setSubject(userId.toString())// 设置 JWT 的过期时间.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))// 使用 HS512 算法签名,并指定密钥.signWith(SIGNING_KEY)// 构建并返回紧凑格式的 JWT 字符串.compact();}/*** 从 JWT 令牌中解析出用户ID。** @param token 需要解析的 JWT 字符串* @return 解析出的用户ID(subject)* @throws JwtException 如果 token 无效或签名不匹配会抛出异常*/public static String parseUserId(String token) {return Jwts.parser()// 设置签名验证所使用的密钥.setSigningKey(SIGNING_KEY)// 解析并验证 JWT 令牌.parseClaimsJws(token)// 获取 JWT 中的负载(claims),并提取 subject(用户名).getBody().getSubject();}/*** 验证 JWT 令牌。** @param token 需要验证的 JWT 令牌* @return 如果令牌有效则返回 true,否则返回 false*/public boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(SIGNING_KEY).build().parseClaimsJws(token);return true;} catch (JwtException | IllegalArgumentException e) {return false;}}}

10. 新增PasswordUtil.java工具类,提供密码加密和验证功能。


import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;/*** 密码工具类* 提供密码加密和验证功能*/
@Component
public class PasswordUtil {/*** 密码编码器实例* 使用BCrypt算法进行密码加密*/private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();/*** 加密密码* @param rawPassword 明文密码* @return 加密后的密码*/public static String encode(String rawPassword) {return passwordEncoder.encode(rawPassword);}/*** 验证密码* @param rawPassword 明文密码* @param encodedPassword 加密后的密码* @return 是否匹配*/public static boolean matches(String rawPassword, String encodedPassword) {return passwordEncoder.matches(rawPassword, encodedPassword);}
}

11.新增UserContextUtil.java用户上下文工具类,提供获取当前登录用户信息的功能。


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.wal.userdemo.config.LoginUser;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;/*** 用户上下文工具类* 提供获取当前登录用户信息的便捷方法*/
@Component
public class UserContextUtil {@Autowiredprivate UserMapper userMapper;/*** 获取当前登录用户信息* 从Spring Security上下文中获取认证信息,解析出登录用户名称,* 然后通过用户Mapper查询完整的用户实体信息* @return 当前用户实体,如果未登录或发生异常则返回null*/public UserEntity getCurrentUser() {try {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();return userMapper.getUserByName(loginUser.getUsername());}} catch (Exception e) {// 记录日志e.printStackTrace();}return null;}/*** 获取当前登录用户名* 通过获取当前用户实体来提取用户名信息* @return 当前用户名,如果未登录则返回null*/public String getUsername() {UserEntity user = getCurrentUser();return user != null ? user.getName() : null;}/*** 获取当前登录用户ID* 通过获取当前用户实体来提取用户ID信息* @return 当前用户ID,如果未登录则返回null*/public Integer getUserId() {UserEntity user = getCurrentUser();return user != null ? user.getId() : null;}
}

12.可以在任意处调用当前登录用户信息,例如,新增用户、修改菜单等。

    /*** 新增 用户** @param userEntity* @return Integer*/@Overridepublic Integer addUser(UserEntity userEntity) {userEntity.setCreateTime(DateUtils.getNowDate());userEntity.setCreateBy(userContextUtil.getUsername());// 添加用户前加密密码if (null != userEntity.getPassword()  && !"".equals(userEntity.getPassword())) {userEntity.setPassword(PasswordUtil.encode(userEntity.getPassword()));}return userMapper.addUser(userEntity);}
 3.日志入库(注解、切面)

1.新增UserLog.java自定义用户操作日志注解。

import java.lang.annotation.*;@Target(ElementType.METHOD) // 注解用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
@Documented
public @interface UserLog {String value() default "";//操作接口描述
}

2.新增 UserLogAspect.java用户操作日志切面。

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
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.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.wal.userdemo.annotation.UserLog;
import org.wal.userdemo.config.LoginUser;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.mapper.UserMapper;
import org.wal.userdemo.service.UserLogService;
import org.wal.userdemo.utils.DateUtils;
import org.wal.userdemo.utils.JwtUtil;
import org.wal.userdemo.utils.Result;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;/*** 用户操作日志切面* 拦截带有 @UserLog 注解的方法,记录用户操作日志。* @author wal* @Date 2025年7月23日17:21:06*/
@Slf4j
@Aspect
@Component
public class UserLogAspect {@Autowiredprivate UserLogService userLogService; // 假设有一个服务类用于保存日志@Autowiredprivate UserMapper userMapper;/*** 定义切入点:匹配所有带有 @UserLog 注解的方法*/@Pointcut("@annotation(org.wal.userdemo.annotation.UserLog)")public void userLog() {}/*** 环绕通知:在目标方法执行前后进行日志记录* @param joinPoint 连接点对象,包含目标方法的信息* @return 目标方法的返回值* @throws Throwable 目标方法可能抛出的异常*/@Around("userLog()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {// 先执行目标方法Object result = null;Exception exception = null;try {result = joinPoint.proceed();} catch (Exception e) {exception = e;throw e;} finally {// 在 finally 块中记录日志,此时认证应该已完成try {recordLog(joinPoint, result, exception);} catch (Exception e) {log.error("记录操作日志失败", e);}}return result;}/*** 记录用户操作日志的核心逻辑* 提取方法信息、请求信息、用户信息,并构建 UserLogEntity 对象保存到数据库* @param joinPoint 连接点对象,包含目标方法的信息* @param result 目标方法的返回结果* @param exception 目标方法抛出的异常(如果有的话)*/private void recordLog(ProceedingJoinPoint joinPoint, Object result, Exception exception) {// 获取方法签名MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();// 获取 @UserLog 注解的值String remark = getUserLogRemark(method);String type = getHttpMethodFromMethod(method);// 获取请求信息ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = null;if (attributes != null) {request = attributes.getRequest();}// 获取当前用户信息UserEntity currentUser = getCurrentUser();String methodUrl = null;String ipAddress = null;String userAgent = null;if (request != null) {methodUrl = request.getMethod() + " " + request.getRequestURI();ipAddress = request.getRemoteAddr();userAgent = request.getHeader("User-Agent");}String status = "0";String text = "";if (exception != null) {status = "1";text = "异常信息:" + exception.getMessage();} else if (result instanceof Result<?>) {Result<?> resultObj = (Result<?>) result;int code = resultObj.getCode();status = (code == 200) ? "0" : "1";try {text = new ObjectMapper().writeValueAsString(result);} catch (Exception e) {text = result.toString();}}// 构建并保存日志实体UserLogEntity userLog = new UserLogEntity();userLog.setType(type);userLog.setMethod(methodUrl);userLog.setStatus(status);userLog.setText(text);userLog.setIpAddress(ipAddress);userLog.setUserAgent(userAgent);if (currentUser != null) {userLog.setUserId(currentUser.getId());userLog.setCreateBy(currentUser.getName());userLog.setCreateTime(DateUtils.getNowDate());}userLog.setCreateTime(new Date());userLog.setDelFlag(0);userLog.setRemark(remark);userLogService.insert(userLog);}/*** 获取 @UserLog 注解中的 remark 值* @param method 目标方法* @return 注解中的值*/private String getUserLogRemark(Method method) {UserLog userLog = method.getAnnotation(UserLog.class);if (userLog != null) {return userLog.value(); // 获取注解中的值}return ""; // 如果没有注解,返回空字符串}/*** 获取当前登录用户信息* 支持从 Spring Security 上下文或 JWT Token 中解析用户信息* @return 当前用户实体,若未找到则返回 null*/private UserEntity getCurrentUser() {HttpServletRequest request = getCurrentRequest();// 方式1:尝试从安全上下文获取UserEntity user = getCurrentUserFromSecurityContext();if (user != null) {return user;}// 方式2:从请求头的 token 解析if (request != null) {user = getCurrentUserFromToken(request);if (user != null) {return user;}}return null;}/*** 获取当前 HTTP 请求对象* @return 当前请求对象,若不存在则返回 null*/private HttpServletRequest getCurrentRequest() {ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();return attributes != null ? attributes.getRequest() : null;}/*** 从 Spring Security 上下文中获取当前用户信息* @return 当前用户实体,若未找到则返回 null*/private UserEntity getCurrentUserFromSecurityContext() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();return userMapper.getUserByName(loginUser.getUsername());}log.error("未找到当前用户信息");return null;}/*** 根据方法上的注解判断 HTTP 请求方法类型* @param method 目标方法* @return HTTP 方法类型字符串(如 GET、POST 等)*/public static String getHttpMethodFromMethod(Method method) {if (method.isAnnotationPresent(GetMapping.class)) {return "GET";} else if (method.isAnnotationPresent(PostMapping.class)) {return "POST";} else if (method.isAnnotationPresent(PutMapping.class)) {return "PUT";} else if (method.isAnnotationPresent(DeleteMapping.class)) {return "DELETE";} else if (method.isAnnotationPresent(PatchMapping.class)) {return "PATCH";} else if (method.isAnnotationPresent(RequestMapping.class)) {return "REQUEST";}return "UNKNOWN";}/*** 从请求头的 JWT Token 中解析当前用户信息* @param request 当前 HTTP 请求对象* @return 当前用户实体,若解析失败或未找到则返回 null*/private UserEntity getCurrentUserFromToken(HttpServletRequest request) {if (request == null) return null;String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7);try {String userId = JwtUtil.parseUserId(token);return userMapper.getUserById(Integer.parseInt(userId));} catch (Exception e) {log.warn("解析Token失败: {}", e.getMessage());return null;}}log.error("未找到Token");return null;}}

3.日志切面实现(以LoginController.java登录接口为例),在需要实现日志记录的接口上加上@UserLog(“接口描述”)即可实现访问该接口时记录日志。

@UserLog("登录接口")@PostMapping("/login")public Result<?> login(@RequestBody LoginReq request) {try {// 使用Spring Security进行用户身份验证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));// 将认证结果存储到安全上下文中SecurityContextHolder.getContext().setAuthentication(authentication);// 从认证结果中获取用户信息String username = authentication.getName();UserEntity user = userService.getUserByName(username);if (user != null) {// 生成JWT token并返回String token = jwtUtil.generateToken(user.getId());return Result.success(token);} else {return Result.error("用户不存在");}} catch (Exception e) {e.printStackTrace();return Result.error("用户名或密码错误");}}

三、前端调整

1.日志管理开发

1.router路由,新增log.js日志管理=>操作日志

export default [{path: 'log',name: 'log',component: () => import('@/view/logmanage/log.vue'),meta: { title: '操作日志', requiresAuth: true }}]

2.请求封装,新增log.js封装日志请求

import request from '@/utils/request';export function getUserLogList(data) {return request({url: '/log/getUserLogList',method: 'post',data: data,});
}
export function getUserLogById(id){return request({url: '/log/getUserLogById',method: 'get',params: {id: id},});
}

 3.在router/index.js中引入路由log.js到index路由下

//省略部分导入import log from './log.js'
Vue.use(Router)const router = new Router({mode: 'history',routes: [{path: '/',name: 'Index',component: Index,redirect: '/login', // 默认重定向到 /homechildren: [{path: '/home',name: 'home',component: () => import('@/view/home.vue'),meta: { title: '首页', requiresAuth: true }},// 其他子路由也可以放在这里...permission,...log]},
// 其他路由配置、、、、、、、

4.新增log.vue实现用户操作日志可视化

<template><div><!-- 查询条件 --><el-form :inline="true" label-position="right" label-width="80px" :model="queryForm"class="query-border-container"><el-row :gutter="20" justify="center"><!-- 操作类型 --><el-col :span="7"><el-form-item label="操作类型"><el-input v-model="queryForm.type" placeholder="请选择操作类型"></el-input></el-form-item></el-col><!-- 操作状态 --><el-col :span="7"><el-form-item label="操作状态"><el-input v-model="queryForm.status" placeholder="请选择操作状态"></el-input></el-form-item></el-col><!-- 操作时间 --><el-col :span="7"><el-form-item label="操作时间"><el-date-picker v-model="queryForm.createTime" type="date" placeholder="选择操作时间"style="width: 100%;"></el-date-picker></el-form-item></el-col><!-- 按钮组 --><el-col :span="7"><el-form-item><div style="display: flex; gap: 10px;"><el-button type="primary" @click="onQuery" size="small">查询</el-button><el-button @click="onReset" size="small">重置</el-button></div></el-form-item></el-col></el-row></el-form><!-- 用户列表 --><el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"v-loading="loading"><el-table-column type="index" label="序号" width="100" align="center"><template #default="scope">{{ (queryForm.page - 1) * queryForm.limit + scope.$index + 1 }}</template></el-table-column><el-table-column prop="type" label="操作类型" width="180" align="center"></el-table-column><el-table-column prop="method" label="操作接口" width="180" align="center"></el-table-column><el-table-column prop="status" label="操作状态" width="180" align="center"></el-table-column><el-table-column prop="text" label="响应结果" width="180" align="center"><template #default="scope"><el-tooltip :content="scope.row.text" placement="top":disabled="!scope.row.text || scope.row.text.length <= 20"><span class="ellipsis-text">{{ scope.row.text && scope.row.text.length > 20 ?scope.row.text.substring(0, 20) + '...' : scope.row.text }}</span></el-tooltip></template></el-table-column><el-table-column prop="userAgent" label="客户端信息" width="180" align="center"><template #default="scope"><el-tooltip :content="scope.row.userAgent" placement="top":disabled="!scope.row.userAgent || scope.row.userAgent.length <= 20"><span class="ellipsis-text">{{ scope.row.userAgent && scope.row.userAgent.length > 20 ?scope.row.userAgent.substring(0, 20) + '...' : scope.row.userAgent }}</span></el-tooltip></template></el-table-column><el-table-column prop="createTime" label="操作时间" width="180" align="center"></el-table-column><el-table-column prop="createBy" label="操作用户" width="180" align="center"></el-table-column><el-table-column label="操作" width="350" align="center"><template #default="scope"><el-button type="primary" size="small" @click="details(scope.$index, scope.row)">详情</el-button></template></el-table-column></el-table><!-- 分页 --><el-pagination background layout="total,sizes,prev, pager, next" :total="total" @size-change="handleSizeChange"@current-change="handleCurrentChange" :page-size.sync="queryForm.limit" :page-sizes="[10, 20, 50, 100]"class="page-border-container"></el-pagination></div></template><script>
import { getUserLogList, getUserLogById } from '@/api/logmanage/log';export default {name: 'logView',data() {return {tableData: [],queryForm: {page: 1,limit: 10,type: '',status: '',createTime: '',},total: 0,loading: false,form: {},};},created() {this.getUserLogList();},methods: {// 获取日志列表getUserLogList() {this.loading = true;getUserLogList(this.queryForm).then(res => {if (res.data.code == 200) {this.tableData = res.data.data;this.total = res.data.total;this.$message.success("获取日志列表成功!");} else {this.$message.error("获取日志列表失败!");}}).finally(() => {this.loading = false;});},// 查询onQuery() {this.getUserLogList();},// 重置表单并查询onReset() {this.queryForm = {page: 1,limit: 10,type: '',status: '',createTime: '',};this.getUserLogList();},details(index, row) {getUserLogById(row.id).then(res => {if (res.data.code == 200) {// this.form = res.data.data;// this.showUser = true;} else {this.$message.error("获取用户信息失败!");}});},//分页器改变handleSizeChange(val) {this.queryForm.limit = val;this.getUserLogList();},//改变页码handleCurrentChange(val) {this.queryForm.page = val;this.getUserLogList();},// 取消按钮:关闭弹窗closeRoleDialog() {this.userId = null;this.selectedRoles = [];this.showRoleDialog = false;},},
};
</script>
<style scoped></style>

四、附:源码

1.源码下载地址

https://gitee.com/wangaolin/user-demo.git

同学们有需要可以自行下载查看,此文章是dev-vue分支

五、结语

        此次开发新引入了Spring Security用来认证和鉴权(没用到),近期工作较忙,近期不再发版,但是项目我还会继续维护,确确实实还有多少我觉得不合理、不够人性化的地方。

后续可能还会加的功能如下:

  • 用户管理的头像、邮箱、电话、水印、信息加密等
  • 菜单管理的支持拖拽、过滤等
  • 日志管理细化、分类
  • 完善首页的数据展示、优化等

有需要的同学可以关注我后续发布的文章和代码维护。 

(注:接定制化开发前后端分离项目,私我) 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91840.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91840.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91840.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ceph 14.2.22 nautilus Balancer 数据平衡

Ceph Balancer (upmap 模式) 启用与配置 在 Ceph Nautilus (14.2.22) 版本中启用和配置 Balancer 的完整步骤 1. 前提检查 检查集群的初始状态和版本。 集群状态 (ceph -s)cluster:id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxhealth: HEALTH_OKservices:mon: 3 daemons, quo…

在Linux上对固态硬盘进行分区、格式化和挂载的步骤

在Linux上对固态硬盘进行分区、格式化和挂载的步骤如下&#xff1a; 插入固态硬盘&#xff1a;将固态硬盘插入计算机的SATA或M.2接口。 确认固态硬盘被识别&#xff1a;打开终端&#xff0c;输入以下命令查看硬盘是否被系统识别 fdisk -l 查找硬盘列表中的固态硬盘&#xf…

用Unity结合VCC更改人物模型出现的BUG

1、上传模型时出现错误经过排查是因为服装发型预制体放到人物模型上之后&#xff0c;物体上自动多了一个空脚本&#xff0c;怀疑是VRC工具箱自动添加的。解决方法&#xff1a;在上传前将带有空脚本的物体上的组件删除即可2、添加头发时出现模型碰撞错误按照【【VRCHAT】从零开始…

k8s之DevicePlugin

解密 Kubernetes Device Plugin&#xff1a;让容器轻松驾驭特殊硬件 在容器化技术飞速发展的今天&#xff0c;容器凭借轻量、隔离、可移植的特性成为应用部署的主流选择。但在实际应用中&#xff0c;当容器需要访问 GPU、FPGA 等特殊硬件资源时&#xff0c;事情就变得不那么简单…

动态规划Day7学习心得

今天给动态规划扫个尾&#xff0c;还有两题。 第一道&#xff1a;647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; 暴力解法 两层for循环&#xff0c;遍历区间起始位置和终止位置&#xff0c;然后还需要一层遍历判断这个区间是不是回文。所以时间复杂度&#xff1a;O…

SpringCloud实战:机器人对战系统架构

基于Spring Cloud的机器人对战 以下是基于Spring Cloud的机器人对战实例相关案例和技术实现方向的整理,涵盖微服务架构设计、通信机制及典型应用场景: 分布式对战系统架构 采用Spring Cloud Alibaba+Nacos实现服务注册与发现,每个机器人实例作为独立微服务部署。通过Open…

LLM 核心能力解构与项目实践指南

大语言模型&#xff08;LLM&#xff09;的爆发式发展&#xff0c;本质上是其核心能力在产业场景中的规模化验证。作为技术博主&#xff0c;本文将系统拆解 LLM 的六大核心能力&#xff0c;结合工业级项目案例&#xff0c;提供从能力映射到工程实现的完整技术路径&#xff0c;并…

retro-go 1.45 编译及显示中文

最近做了个使用 retro-go 的开源掌机 基于ESP32-S3的C19掌机&#xff08;适配GBC外壳&#xff09; - 立创开源硬件平台 &#xff0c;做完后用提供的固件发现屏幕反显了&#xff0c;估计是屏幕型号不太对&#xff0c;随即自己拉 retro-go 官方库来编译&#xff0c;拉取的最新的 …

中州养老项目:Mybatis自动填充拦截器

功能:在新增护理项目的时候,创建人,创建时间和修改时间字段会自动拦截填充,这些公共字段可以省去我们一个一个处理的麻烦依靠:AutoFillInterceptor拦截器,MybatisConfig配置类第一步:我们需要借助一个MybatisConfig,configuration标志着这是一个配置类,我们需要将autoFillInter…

[创业之路-527]:什么是产品技术成熟度曲线?

产品技术成熟度曲线&#xff08;Gartner Hype Cycle&#xff09;是由全球知名咨询机构Gartner提出的工具&#xff0c;用于可视化展示新兴技术从诞生到成熟的发展轨迹&#xff0c;以及市场对其预期和实际采用趋势的变化。该曲线通过五个阶段刻画技术生命周期&#xff0c;帮助企业…

VScode对Ubuntu用root账号进行SSH远程连接开发

由于linux服务器大部分都是基于命令行的操作&#xff0c;缺乏比较方便好用的编辑工具&#xff0c;对于经常在linux服务器上做开发的同学来说直接在服务器上进行开发或配置文件的修改还不是特别的方便。虽然linux上有vi或vim比起图形化的编辑工具体验感还是不是很好。作为程序员…

【物联网】基于树莓派的物联网开发【20】——树莓派控制DHT11温湿度传感器实战

传感器概述 DHT11是一款有已校准数字信号输出的温湿度传感器。 其精度湿度5%RH&#xff0c; 温度2℃&#xff0c;量程湿度20-90%RH&#xff0c; 温度0~50℃。分为3个接口&#xff0c;分别为&#xff1a;VCC, DATA, GND。 产品图片主要用途 检测环境温湿度 GPIO控制DHT11温湿度传…

AI原生数据库:告别SQL的新时代来了?

在2025年的今天&#xff0c;生成式AI的浪潮正以前所未有的力量重塑着各行各业。从代码生成到艺术创作&#xff0c;大型语言模型&#xff08;LLM&#xff09;的能力边界不断被拓宽。现在&#xff0c;这股浪潮正涌向信息技术领域最古老、最核心的基石之一&#xff1a;数据库。一个…

题单【模拟与高精度】

P1042 [NOIP 2003 普及组] 乒乓球 P1042 [NOIP 2003 普及组] 乒乓球 - 洛谷 #include<bits/stdc.h> using namespace std;char C; string S; int n,A,B;void Work(int Lim) {for(char i:S){if(iW) A;if(iL) B;if(max(A,B)>Lim && abs(A-B)>2){cout<<…

数据结构学习基础和从包装类缓存到泛型擦除的避坑指南

目录 1.数据结构的概念和算法 1.1 数据结构的概念 1.2 数据结构的集合框架 1.3 算法 1.3.1 时间复杂度 1.3.2 空间复杂度 2.包装类 2.1 为什么需要包装类&#xff1f; 2.2 装箱和拆箱 3. 初识泛型 3.1 认识泛型 3.2 泛型类的使用 3.3 泛型的编译 3.4 通配符 3.4.1 …

网络安全基础知识【6】

什么是防火墙1.防火墙指的是一个由软件和硬件设备组合而成、在内部网和外部网之间、 专用网与公共网之间的界面上构造的保护屏障 2.防火墙实际上是一种隔离技术 3.防火墙重要的特征是增加了区域的概念防火墙的定义 隔离可信与不可信网络的设备/软件&#xff0c;基于策略控制流量…

Apache Doris数据库——大数据技术

Apache Doris一、简介1.1、Apache Doris简介1.2、Apache Doris 与传统大数据架构相比1.3、doris是java团队掌控大数据能力最优选择1.4、 OLTP&#xff08;在线事务处理&#xff09; 与 OLAP&#xff08;在线分析处理&#xff09;1.5、发展历程1.6、应用现状1.7、整体架构1.7.1、…

Conda和pip的使用记录

Conda和pip的使用记录一、创建新的 Conda 环境二、激活环境三、安装其他包&#xff08;可选&#xff09;四、查看已有环境五、删除环境&#xff08;可选&#xff09;⚙️ Conda 下载缓慢的解决方案&#xff08;推荐使用国内镜像&#xff09;&#x1f527; 方法一&#xff1a;**…

详解Python标准库之互联网数据处理

详解Python标准库之互联网数据处理 在互联网时代&#xff0c;数据的产生、传输和处理无处不在。从电子邮件的收发到 API 接口的数据交换&#xff0c;从二进制数据的编码到 MIME 类型的识别&#xff0c;Python 标准库提供了一整套强大的工具集&#xff0c;帮助开发者轻松应对各种…

适 配 器 模 式

前阵子&#xff0c;笔者在网上淘来一个二手显示屏来搭配我装好的主机&#xff0c;但是送到手上后我却找不到电源适配器的踪迹。于是我就在家找了根电源线接上了显示屏&#xff0c;倒是能亮&#xff0c;就是屏幕闪得和机关枪似的。这是因为我的显示屏需要12V的供电&#xff0c;我…