app登录接口实现,基于JWT的APP登录认证系统实现方案

首先是 JWT

java 复制代码
package org.springblade.community.util;




import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.WeakKeyException;
import org.springblade.community.config.CommunityLoginConfig;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 社区端JWT工具类(适配 JJWT 0.13.0 版本)
 */
@Component
public class CommunityJwtUtil {

	/**
	 * 生成token(0.13.0 版本正确写法)
	 */
	public String generateToken(Long patientId, String phone) {
		try {
			// 1. 生成合规的密钥(0.13.0 要求 HS256 密钥必须 ≥ 32 字节)
			SecretKey secretKey = generateSecretKey();

			// 2. 构建用户信息Claims
			Map<String, Object> claims = new HashMap<>();
			claims.put("patientId", patientId);
			claims.put("phone", phone);

			// 3. 生成Token(0.13.0 无需显式指定算法,密钥已包含算法信息)
			return Jwts.builder()
				.claims(claims) // 替代旧版 setClaims
				.subject(phone)
				.issuedAt(new Date())
				.expiration(new Date(System.currentTimeMillis() + CommunityLoginConfig.JWT_EXPIRE))
				.signWith(secretKey) // 仅需传入密钥,自动匹配算法
				.compact();
		} catch (WeakKeyException e) {
			throw new RuntimeException("JWT密钥长度不足,HS256算法要求密钥至少32字节", e);
		} catch (Exception e) {
			throw new RuntimeException("生成Token失败", e);
		}
	}

	/**
	 * 解析token获取Claims(0.13.0 版本正确写法)
	 */
	public Claims parseToken(String token) {
		try {
			SecretKey secretKey = generateSecretKey();
			// 0.13.0 版本 parserBuilder() 用法正确,核心是密钥合规
			return Jwts.parser() // 0.13.0 也可以用 parser(),和 parserBuilder() 等效
				.verifyWith(secretKey) // 替代旧版 setSigningKey
				.build()
				.parseSignedClaims(token) // 替代旧版 parseClaimsJws
				.getPayload(); // 替代旧版 getBody
		} catch (WeakKeyException e) {
			throw new RuntimeException("JWT密钥长度不足", e);
		} catch (Exception e) {
			throw new RuntimeException("解析Token失败:" + e.getMessage(), e);
		}
	}

	/**
	 * 验证token是否过期
	 */
	public boolean isTokenExpired(Claims claims) {
		return claims.getExpiration().before(new Date());
	}

	/**
	 * 生成合规的JWT密钥(适配 0.13.0 版本)
	 */
	private SecretKey generateSecretKey() {
		String secret = CommunityLoginConfig.JWT_SECRET;
		// 1. 检查密钥长度,不足则补全(HS256 要求 ≥ 32 字节)
		if (secret.length() < 32) {
			// 补全到32字节(示例:不足部分用下划线填充,你也可以自定义规则)
			secret = String.format("%-32s", secret).replace(' ', '_');
		}
		// 2. 生成密钥(0.13.0 推荐用 Keys.hmacShaKeyFor)
		return Keys.hmacShaKeyFor(secret.getBytes());
	}


	/**
	 * 刷新Token(核心方法)
	 * @param oldToken 旧的Token(无需Bearer前缀)
	 * @return 新的Token
	 */
	public String refreshToken(String oldToken) {
		try {
			// 1. 解析旧Token(即使过期也先解析,后续单独判断过期时间)
			Claims claims = parseTokenIgnoreExpiration(oldToken);

			// 2. 获取旧Token中的用户信息
			Long patientId = Long.valueOf(claims.get("patientId").toString());
			String phone = claims.get("phone").toString();

			// 3. 可选:校验旧Token过期时间(允许过期30分钟内刷新,避免无限刷新)
			Date expiration = claims.getExpiration();
			long expiredTime = System.currentTimeMillis() - expiration.getTime();
			long maxRefreshExpiredTime = 30 * 60 * 1000; // 30分钟,可配置化
			if (expiredTime > maxRefreshExpiredTime) {
				throw new RuntimeException("Token已过期过久,无法刷新,请重新登录");
			}

			// 4. 生成新Token(复用原有生成逻辑)
			return generateToken(patientId, phone);
		} catch (Exception e) {
			throw new RuntimeException("刷新Token失败:" + e.getMessage(), e);
		}
	}

	/**
	 * 解析Token(忽略过期校验,用于刷新场景)
	 */
	private Claims parseTokenIgnoreExpiration(String token) {
		SecretKey secretKey = generateSecretKey();
		return Jwts.parser()
			.verifyWith(secretKey)
			.build()
			.parseSignedClaims(token)
			.getPayload();
	}
}

还有jwt配置:

java 复制代码
package org.springblade.community.config;



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 社区端登录相关配置
 */
@Configuration
public class CommunityLoginConfig {

	/**
	 * 密码加密器
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	// JWT配置常量(也可放到application.yml中)
	public static final String JWT_SECRET = "community_login_2026@secret_key"; // 密钥,建议放到配置文件
	public static final long JWT_EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 过期时间7天
	public static final String JWT_HEADER = "Authorization"; // token请求头
}

这样就能实现账号的token生成和 解析了

然后是登录上下文工具:

java 复制代码
package org.springblade.community.context;




import lombok.Data;

/**
 * 社区登录用户上下文(ThreadLocal存储,保证请求线程隔离)
 */
public class CommunityUserContext {

	// ThreadLocal存储当前登录用户信息
	private static final ThreadLocal<CommunityLoginUser> USER_THREAD_LOCAL = new ThreadLocal<>();

	/**
	 * 设置当前登录用户
	 */
	public static void setUser(CommunityLoginUser user) {
		USER_THREAD_LOCAL.set(user);
	}

	/**
	 * 获取当前登录用户
	 */
	public static CommunityLoginUser getUser() {
		return USER_THREAD_LOCAL.get();
	}

	/**
	 * 获取当前登录用户ID
	 */
	public static Long getCurrentPatientId() {
		CommunityLoginUser user = getUser();
		return user == null ? null : user.getPatientId();
	}

	/**
	 * 获取当前登录用户手机号
	 */
	public static String getCurrentPhone() {
		CommunityLoginUser user = getUser();
		return user == null ? null : user.getPhone();
	}

	/**
	 * 清除ThreadLocal(防止内存泄漏)
	 */
	public static void clear() {
		USER_THREAD_LOCAL.remove();
	}

	/**
	 * 登录用户信息封装类
	 */
	@Data
	public static class CommunityLoginUser {
		/**
		 * 患者ID
		 */
		private Long patientId;

		/**
		 * 手机号
		 */
		private String phone;


	}
}

再然后就是接口了:

java 复制代码
package org.springblade.community.controller;




import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.context.CommunityUserContext;
import org.springblade.community.pojo.dto.AdminResetPwdDTO;
import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;

import org.springblade.community.pojo.dto.RefreshTokenDTO;
import org.springblade.community.service.ICommunityLoginService;
import org.springblade.core.tool.api.R;
import org.springblade.patient.pojo.vo.PatientInfoVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 社区端登录接口
 */
@RestController
@RequestMapping("/community") // 社区端专属前缀
public class CommunityLoginController {

	@Autowired
	private ICommunityLoginService communityLoginService;

	/**
	 * 社区患者登录接口
	 */
	@PostMapping("/login")
	public R login(@Validated @RequestBody CommunityLoginDTO loginDTO) {
		try {
			PatientInfoVO loginVO = communityLoginService.login(loginDTO);
			return R.data(loginVO);
		} catch (RuntimeException e) {
			return R.fail(e.getMessage());
		}
	}

	/**
	 * 社区患者Token刷新接口
	 */
	@PostMapping("/refresh-token")
	public R refreshToken(@RequestBody RefreshTokenDTO refreshTokenDTO) {
		try {
			// 1. 提取旧Token(兼容前端传递的Bearer前缀)
			String oldToken = refreshTokenDTO.getOldToken();
			if (oldToken.startsWith("Bearer ")) {
				oldToken = oldToken.substring(7).trim();
			}

			// 2. 调用Service刷新Token
			String newToken = communityLoginService.refreshToken(oldToken);

			// 3. 封装返回结果(包含新Token和过期时间)
			Map<String, Object> result = new HashMap<>();
			result.put("newToken", newToken);
			result.put("expireTime", CommunityLoginConfig.JWT_EXPIRE);

			return R.data(result);
		} catch (RuntimeException e) {
			return R.fail(e.getMessage());
		}
	}



	/**
	 * 重置密码接口(用户自主重置/管理员重置)
	 * 用户自主重置:需要验证旧密码,确保是本人操作;
	 * 管理员重置:无需验证旧密码,直接修改用户密码。
	 */
	@PostMapping("/reset-password")
	public R resetPassword(@Validated @RequestBody CommunitySetPasswordDTO passwordDTO) {
		try {
			communityLoginService.setPassword(passwordDTO);
			return R.success("密码修改成功");
		} catch (RuntimeException e) {
			return R.fail(e.getMessage());
		}
	}


	// ========== 新增测试接口:获取当前登录用户信息 ==========
	/**
	 * 测试接口:获取当前登录用户的详细信息(需携带Token)
	 */
	@GetMapping("/test/current-user")
	public R getCurrentUserInfo() {
		try {
			// 1. 从ThreadLocal中获取当前登录用户上下文
			CommunityUserContext.CommunityLoginUser loginUser = CommunityUserContext.getUser();

			if (loginUser == null) {
				return R.fail("未获取到用户信息,请先登录");
			}

			// 2. 封装用户信息返回(可按需扩展字段)
			Map<String, Object> userInfo = new HashMap<>();
			userInfo.put("患者ID", loginUser.getPatientId());
			userInfo.put("手机号", loginUser.getPhone());
			// 可选:如果上下文存了完整PatientInfoEntity,可返回更多字段
			// userInfo.put("完整用户信息", loginUser.getPatientInfo());

			return R.data(userInfo);
		} catch (Exception e) {
			return R.fail("获取用户信息失败:" + e.getMessage());
		}
	}

}
java 复制代码
package org.springblade.community.service;

import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;
import org.springblade.patient.pojo.vo.PatientInfoVO;


/**
 * 社区端登录服务接口
 */
public interface ICommunityLoginService {

	/**
	 * 社区患者登录
	 */
	PatientInfoVO login(CommunityLoginDTO loginDTO);

	/**
	 * 设置/重置患者密码
	 * 用户自主重置:需要验证旧密码,确保是本人操作;
	 * 管理员重置:无需验证旧密码,直接修改用户密码。
	 */
	void setPassword(CommunitySetPasswordDTO passwordDTO);

	/**
	 * 新增患者(注册)时初始化密码(供管理员/注册接口调用)
	 */
	String encryptPassword(String rawPassword);


	String refreshToken(String oldToken);
}
java 复制代码
package org.springblade.community.service.impl;



import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;
import org.springblade.community.service.ICommunityLoginService;
import org.springblade.community.util.CommunityJwtUtil;
import org.springblade.patient.pojo.entity.PatientInfoEntity;
import org.springblade.patient.pojo.vo.PatientInfoVO;
import org.springblade.patient.service.IPatientInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * 社区端登录服务实现
 */
@Service
public class CommunityLoginServiceImpl implements ICommunityLoginService {

	@Autowired
	private IPatientInfoService patientInfoService;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Autowired
	private CommunityJwtUtil jwtUtil;

	@Override
	public PatientInfoVO login(CommunityLoginDTO loginDTO) {
		// 1. 根据手机号查询患者信息(排除已删除、状态异常的用户)
		LambdaQueryWrapper<PatientInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
		queryWrapper.eq(PatientInfoEntity::getPhone, loginDTO.getPhone())
			.eq(PatientInfoEntity::getIsDeleted, 0) // 未删除
			.isNotNull(PatientInfoEntity::getStatus) // 状态不为空
			.eq(PatientInfoEntity::getStatus, 1); // 假设1为正常状态,根据你的业务调整

		PatientInfoEntity patient = patientInfoService.getOne(queryWrapper);
		if (patient == null) {
			throw new RuntimeException("手机号不存在或账号已禁用");
		}

		// 2. 校验密码(数据库中密码是加密存储的,此处比对)
		if (!passwordEncoder.matches(loginDTO.getPassword(), patient.getPassword())) {
			throw new RuntimeException("密码错误");
		}

		// 3. 生成JWT token
		String token = jwtUtil.generateToken(patient.getId(), patient.getPhone());

		// 4. 封装返回结果
		PatientInfoVO loginVO = new PatientInfoVO();
		loginVO.setId(patient.getId());
		loginVO.setName(patient.getName());
		loginVO.setPhone(patient.getPhone());
		loginVO.setToken(token);
		loginVO.setExpireTime(CommunityLoginConfig.JWT_EXPIRE);

		return loginVO;
	}

	/**
	 * 设置/重置密码核心逻辑
	 */
	@Override
	public void setPassword(CommunitySetPasswordDTO passwordDTO) {
		// 1. 基础参数校验
		if (!passwordDTO.getNewPassword().equals(passwordDTO.getConfirmPassword())) {
			throw new RuntimeException("新密码与确认密码不一致");
		}

		// 2. 查询用户是否存在
		LambdaQueryWrapper<PatientInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
		queryWrapper.eq(PatientInfoEntity::getPhone, passwordDTO.getPhone())
			.eq(PatientInfoEntity::getIsDeleted, 0);
		PatientInfoEntity patient = patientInfoService.getOne(queryWrapper);
		if (patient == null) {
			throw new RuntimeException("用户不存在");
		}

		// 3. 非管理员重置时,验证旧密码
		if (!passwordDTO.getIsAdminReset()) {
			if (!StringUtils.hasText(passwordDTO.getOldPassword())) {
				throw new RuntimeException("请输入旧密码");
			}
			if (!passwordEncoder.matches(passwordDTO.getOldPassword(), patient.getPassword())) {
				throw new RuntimeException("旧密码错误");
			}
		}

		// 4. 加密新密码并更新数据库
		String encryptedNewPwd = encryptPassword(passwordDTO.getNewPassword());
		LambdaUpdateWrapper<PatientInfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
		updateWrapper.eq(PatientInfoEntity::getId, patient.getId())
			.set(PatientInfoEntity::getPassword, encryptedNewPwd)
			// 可选:更新修改时间
			.set(PatientInfoEntity::getUpdateTime, new java.util.Date());
		boolean updateSuccess = patientInfoService.update(updateWrapper);
		if (!updateSuccess) {
			throw new RuntimeException("密码修改失败");
		}
	}

	/**
	 * 密码加密工具方法(供注册/重置密码调用)
	 */
	@Override
	public String encryptPassword(String rawPassword) {
		// BCrypt加密(不可逆,每次加密结果不同,但校验时可匹配)
		return passwordEncoder.encode(rawPassword);
	}


	@Override
	public String refreshToken(String oldToken) {
		// 1. 校验旧Token非空
		if (oldToken == null || oldToken.trim().isEmpty()) {
			throw new RuntimeException("旧Token不能为空");
		}

		// 2. 调用JwtUtil刷新Token(内部已做合法性校验)
		try {
			return jwtUtil.refreshToken(oldToken);
		} catch (Exception e) {
			throw new RuntimeException("Token刷新失败:" + e.getMessage());
		}
	}
}

接下来是一些dto,vo 这里我就放一起了

java 复制代码
package org.springblade.community.pojo.dto;



import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;


/**
 * 管理员重置患者密码请求参数
 */
@Data
public class AdminResetPwdDTO {
	/**
	 * 患者ID(二选一:ID或手机号必填)
	 */
	private Long patientId;

	/**
	 * 患者手机号(二选一:ID或手机号必填)
	 */
	@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
	private String phone;

	/**
	 * 新密码(必填,强制复杂度)
	 */
	@NotBlank(message = "新密码不能为空")
	@Pattern(regexp = "^[a-zA-Z0-9@#$%^&*]{6,20}$", message = "新密码需6-20位,包含字母/数字/特殊字符")
	private String newPassword;

	/**
	 * 操作管理员ID(必填,用于日志记录)
	 */
	@NotNull(message = "操作人ID不能为空")
	private Long operatorId;

	/**
	 * 操作备注(可选,如"用户忘记密码,人工重置")
	 */
	private String remark;
}
java 复制代码
package org.springblade.community.pojo.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;


/**
 * 社区端患者登录请求参数
 */
@Data
public class CommunityLoginDTO {

	/**
	 * 手机号(必填,格式校验)
	 */
	@NotBlank(message = "手机号不能为空")
	@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
	private String phone;

	/**
	 * 登录密码(必填)
	 */
	@NotBlank(message = "密码不能为空")
	private String password;
}
java 复制代码
package org.springblade.community.pojo.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;


/**
 * 社区患者设置/重置密码请求参数
 */
@Data
public class CommunitySetPasswordDTO {
	/**
	 * 手机号(必填,用于定位用户)
	 */
	@NotBlank(message = "手机号不能为空")
	@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
	private String phone;

	/**
	 * 旧密码(用户自主重置时必填,管理员重置时可选)
	 */
	private String oldPassword;

	/**
	 * 新密码(必填,建议增加复杂度校验)
	 */
	@NotBlank(message = "新密码不能为空")
	@Pattern(regexp = "^[a-zA-Z0-9@#$%^&*]{6,20}$", message = "新密码需6-20位,包含字母/数字/特殊字符")
	private String newPassword;

	/**
	 * 确认新密码(必填,需和newPassword一致)
	 */
	@NotBlank(message = "确认密码不能为空")
	private String confirmPassword;

	/**
	 * 是否管理员重置(true=无需验证旧密码,false=需验证)
	 */
	private Boolean isAdminReset = false;
}
java 复制代码
package org.springblade.community.pojo.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class RefreshTokenDTO {
	@NotBlank(message = "旧Token不能为空")
	private String oldToken;
}

接口能跑了之后 ,得需要一个拦截器 拦住这个接口吧:

java 复制代码
package org.springblade.community.interceptor;


import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.context.CommunityUserContext;
import org.springblade.community.util.CommunityJwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;



/**
 * 社区端Token拦截器(验证登录状态)
 */
@Slf4j
@Component
public class CommunityTokenInterceptor implements HandlerInterceptor {

	@Autowired
	private CommunityJwtUtil jwtUtil;

	/**
	 * 请求处理前验证Token
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 1. 从请求头获取Token(格式:Bearer xxxxx)
		String token = request.getHeader(CommunityLoginConfig.JWT_HEADER);
		if (token == null || !token.startsWith("Bearer ")) {
			// Token为空或格式错误,返回401未授权
			response.setContentType("application/json;charset=UTF-8");
			response.getWriter().write("{\"code\":401,\"msg\":\"未登录或Token格式错误\",\"data\":null}");
			log.warn("社区端未登录或Token格式错误:{}, 请求路径:{}", token, request.getRequestURI());
			return false;
		}

		// 2. 提取纯Token(去掉Bearer前缀)
		String pureToken = token.substring(7);

		try {
			// 3. 解析Token
			Claims claims = jwtUtil.parseToken(pureToken);

			// 4. 验证Token是否过期
			if (jwtUtil.isTokenExpired(claims)) {
				response.setContentType("application/json;charset=UTF-8");
				response.getWriter().write("{\"code\":401,\"msg\":\"Token已过期\",\"data\":null}");
				return false;
			}

			// 5. 封装用户信息并存入ThreadLocal
			CommunityUserContext.CommunityLoginUser loginUser = new CommunityUserContext.CommunityLoginUser();
			loginUser.setPatientId(Long.valueOf(claims.get("patientId").toString()));
			loginUser.setPhone(claims.get("phone").toString());
			// 扩展:可根据patientId查询完整用户信息存入(按需)
			// loginUser.setPatientInfo(patientInfoService.getById(loginUser.getPatientId()));

			CommunityUserContext.setUser(loginUser);

			// 6. Token验证通过,放行请求
			return true;
		} catch (Exception e) {
			// Token解析失败(伪造、篡改等)
			response.setContentType("application/json;charset=UTF-8");
			response.getWriter().write("{\"code\":401,\"msg\":\"Token无效:" + e.getMessage() + "\",\"data\":null}");
			return false;
		}
	}

	/**
	 * 请求处理完成后清除ThreadLocal(防止内存泄漏)
	 */
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
		CommunityUserContext.clear();
	}
}

然后再写一个webConfig配置,拦住所有这个开头得接口,就留下登录接口就行:

java 复制代码
package org.springblade.community.config;


import org.springblade.community.interceptor.CommunityTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 社区端拦截器配置
 */
@Configuration
public class CommunityWebConfig implements WebMvcConfigurer {

	@Autowired
	private CommunityTokenInterceptor communityTokenInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 注册社区端Token拦截器
		registry.addInterceptor(communityTokenInterceptor)
			// 拦截所有社区端接口
			.addPathPatterns("/community/**")
			// 放行登录接口(无需Token即可访问)
			.excludePathPatterns("/community/login");
		// 可选:放行其他无需登录的接口(如验证码、注册等)
		// .excludePathPatterns("/api/community/sendCode", "/api/community/register");
	}
}
相关推荐
云上凯歌2 小时前
01_AI工具平台项目概述.md
人工智能·python·uni-app
WangYaolove13142 小时前
基于图像取证技术研究与实现(源码+文档)
python·django·毕业设计·源码·计算机源码
无籽西瓜a2 小时前
ArrayList和LinkedList的区别
java
程序员敲代码吗2 小时前
用Python监控系统日志并发送警报
jvm·数据库·python
qwerasda1238522 小时前
YOLO13-SEG-RFAConv:隧道围岩病理缺陷识别的改进方法与底层逻辑
python
Elieal2 小时前
@Api 系列注解
java·开发语言
Python大数据分析@2 小时前
Claude Code、Cursor、Trae、OpenCode怎么选?
python
Remember_9932 小时前
【数据结构】深入理解Map和Set:从搜索树到哈希表的完整解析
java·开发语言·数据结构·算法·leetcode·哈希算法·散列表
小楼v2 小时前
消息队列的核心概念与应用(RabbitMQ快速入门)
java·后端·消息队列·rabbitmq·死信队列·交换机·安装步骤