feat(login): 新增用户登录日志记录功能

新增 KeyboardUserLoginLog 实体、Mapper、Service 及 XML,扩展 Apple 与普通登录接口,自动记录 IP、UA、平台、OS 及新用户标识。
This commit is contained in:
2025-12-11 20:16:20 +08:00
parent 071e130a45
commit 07ff9a5ff2
10 changed files with 286 additions and 18 deletions

View File

@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -47,8 +48,8 @@ public class UserController {
@PostMapping("/appleLogin")
@Operation(summary = "苹果登录", description = "苹果登录接口")
@Parameter(name = "code", required = true, description = "苹果登录凭证", example = "123456")
public BaseResponse<KeyboardUserRespVO> appleLogin(@RequestBody AppleLoginReq appleLoginReq) throws Exception {
return ResultUtils.success(appleService.login(appleLoginReq.getIdentityToken()));
public BaseResponse<KeyboardUserRespVO> appleLogin(@RequestBody AppleLoginReq appleLoginReq, HttpServletRequest request) throws Exception {
return ResultUtils.success(appleService.login(appleLoginReq.getIdentityToken(), request));
}
@GetMapping("/logout")
@@ -60,8 +61,8 @@ public class UserController {
@PostMapping("/login")
@Operation(summary = "登录", description = "登录接口")
public BaseResponse<KeyboardUserRespVO> login(@RequestBody UserLoginDTO userLoginDTO) {
return ResultUtils.success(userService.login(userLoginDTO));
public BaseResponse<KeyboardUserRespVO> login(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) {
return ResultUtils.success(userService.login(userLoginDTO, request));
}
@PostMapping("/updateInfo")

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yolo.keyborad.model.entity.KeyboardUserLoginLog;
/*
* @author: ziin
* @date: 2025/12/11 20:09
*/
public interface KeyboardUserLoginLogMapper extends BaseMapper<KeyboardUserLoginLog> {
}

View File

@@ -0,0 +1,89 @@
package com.yolo.keyborad.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Date;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/12/11 20:09
*/
@Schema
@Data
@TableName(value = "keyboard_user_login_log")
public class KeyboardUserLoginLog {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
@Schema(description="主键")
private Long id;
/**
* 用户 ID关联到 keyboard_user 表
*/
@TableField(value = "user_id")
@Schema(description="用户 ID关联到 keyboard_user 表")
private Long userId;
/**
* 登录时间
*/
@TableField(value = "login_time")
@Schema(description="登录时间")
private Date loginTime;
/**
* 登录的 IP 地址
*/
@TableField(value = "ip_address")
@Schema(description="登录的 IP 地址")
private String ipAddress;
/**
* 用户设备信息
*/
@TableField(value = "device_info")
@Schema(description="用户设备信息")
private String deviceInfo;
/**
* 操作系统
*/
@TableField(value = "os")
@Schema(description="操作系统")
private String os;
/**
* 设备平台iOS 或 Android
*/
@TableField(value = "platform")
@Schema(description="设备平台iOS 或 Android")
private String platform;
/**
* 登录状态,成功或失败
*/
@TableField(value = "\"status\"")
@Schema(description="登录状态,成功或失败")
private String status;
/**
* 记录创建时间
*/
@TableField(value = "created_at")
@Schema(description="记录创建时间")
private Date createdAt;
/**
* 记录更新时间
*/
@TableField(value = "updated_at")
@Schema(description="记录更新时间")
private Date updatedAt;
}

View File

@@ -1,6 +1,7 @@
package com.yolo.keyborad.service;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import jakarta.servlet.http.HttpServletRequest;
/**
* Apple相关API
@@ -14,6 +15,7 @@ public interface IAppleService {
* 登录
*
* @param identityToken JWT身份令牌
* @param request HTTP请求
*/
KeyboardUserRespVO login(String identityToken) throws Exception;
KeyboardUserRespVO login(String identityToken, HttpServletRequest request) throws Exception;
}

View File

@@ -0,0 +1,23 @@
package com.yolo.keyborad.service;
import com.yolo.keyborad.model.entity.KeyboardUserLoginLog;
import com.baomidou.mybatisplus.extension.service.IService;
/*
* @author: ziin
* @date: 2025/12/11 20:09
*/
public interface KeyboardUserLoginLogService extends IService<KeyboardUserLoginLog>{
/**
* 记录用户登录信息
* @param userId 用户ID
* @param ipAddress IP地址
* @param deviceInfo 设备信息
* @param os 操作系统
* @param platform 平台(iOS/Android)
* @param status 登录状态
*/
void recordLoginLog(Long userId, String ipAddress, String deviceInfo, String os, String platform, String status);
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.yolo.keyborad.model.dto.user.*;
import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import jakarta.servlet.http.HttpServletRequest;
/*
* @author: ziin
@@ -15,7 +16,7 @@ public interface UserService extends IService<KeyboardUser> {
KeyboardUser createUserWithSubjectId(String sub);
KeyboardUserRespVO login(UserLoginDTO userLoginDTO);
KeyboardUserRespVO login(UserLoginDTO userLoginDTO, HttpServletRequest request);
Boolean updateUserInfo(KeyboardUserReq keyboardUser);

View File

@@ -11,11 +11,11 @@ import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import com.yolo.keyborad.service.IAppleService;
import com.yolo.keyborad.service.KeyboardCharacterService;
import com.yolo.keyborad.service.KeyboardUserLoginLogService;
import com.yolo.keyborad.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import io.jsonwebtoken.*;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -40,13 +40,17 @@ public class AppleServiceImpl implements IAppleService {
@Resource
private UserService userService;
@Resource
private KeyboardUserLoginLogService loginLogService;
/**
* 登录
*
* @param identityToken JWT身份令牌
* @param request HTTP请求
*/
@Override
public KeyboardUserRespVO login(String identityToken) throws Exception {
public KeyboardUserRespVO login(String identityToken, HttpServletRequest request) throws Exception {
// 1. 清理一下 token防止前后多了引号/空格
identityToken = identityToken.trim();
@@ -88,13 +92,51 @@ public class AppleServiceImpl implements IAppleService {
// 返回用户标识符
if (result) {
KeyboardUser user = userService.selectUserWithSubjectId(sub);
boolean isNewUser = false;
if (user == null) {
KeyboardUser newUser = userService.createUserWithSubjectId(sub);
KeyboardUserRespVO keyboardUserRespVO = BeanUtil.copyProperties(newUser, KeyboardUserRespVO.class);
StpUtil.login(newUser.getId());
keyboardUserRespVO.setToken(StpUtil.getTokenValueByLoginId(newUser.getId()));
return keyboardUserRespVO;
user = userService.createUserWithSubjectId(sub);
isNewUser = true;
}
// 记录登录日志
try {
String ipAddress = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
String platform = "Unknown";
String os = "Unknown";
if (userAgent != null) {
if (userAgent.contains("iOS")) {
platform = "iOS";
} else if (userAgent.contains("Android")) {
platform = "Android";
}
if (userAgent.contains("Windows")) {
os = "Windows";
} else if (userAgent.contains("Mac OS")) {
os = "Mac OS";
} else if (userAgent.contains("Linux")) {
os = "Linux";
} else if (userAgent.contains("iOS")) {
os = "iOS";
} else if (userAgent.contains("Android")) {
os = "Android";
}
}
loginLogService.recordLoginLog(
user.getId(),
ipAddress,
userAgent,
os,
platform,
isNewUser ? "APPLE_NEW_USER" : "SUCCESS"
);
} catch (Exception e) {
log.error("记录Apple登录日志失败", e);
}
KeyboardUserRespVO keyboardUserRespVO = BeanUtil.copyProperties(user, KeyboardUserRespVO.class);
StpUtil.login(user.getId());
keyboardUserRespVO.setToken(StpUtil.getTokenValueByLoginId(user.getId()));

View File

@@ -0,0 +1,33 @@
package com.yolo.keyborad.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.mapper.KeyboardUserLoginLogMapper;
import com.yolo.keyborad.model.entity.KeyboardUserLoginLog;
import com.yolo.keyborad.service.KeyboardUserLoginLogService;
/*
* @author: ziin
* @date: 2025/12/11 20:09
*/
@Service
public class KeyboardUserLoginLogServiceImpl extends ServiceImpl<KeyboardUserLoginLogMapper, KeyboardUserLoginLog> implements KeyboardUserLoginLogService{
@Override
public void recordLoginLog(Long userId, String ipAddress, String deviceInfo, String os, String platform, String status) {
KeyboardUserLoginLog loginLog = new KeyboardUserLoginLog();
loginLog.setUserId(userId);
loginLog.setIpAddress(ipAddress);
loginLog.setDeviceInfo(deviceInfo);
loginLog.setOs(os);
loginLog.setPlatform(platform);
loginLog.setStatus(status);
loginLog.setLoginTime(new java.util.Date());
loginLog.setCreatedAt(new java.util.Date());
loginLog.setUpdatedAt(new java.util.Date());
this.save(loginLog);
}
}

View File

@@ -15,15 +15,14 @@ import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.entity.KeyboardUserWallet;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import com.yolo.keyborad.service.KeyboardCharacterService;
import com.yolo.keyborad.service.KeyboardUserLoginLogService;
import com.yolo.keyborad.service.KeyboardUserWalletService;
import com.yolo.keyborad.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import com.yolo.keyborad.utils.RedisUtil;
import com.yolo.keyborad.utils.SendMailUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.K;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -58,6 +57,9 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
@Resource
private KeyboardUserWalletService walletService;
@Resource
private KeyboardUserLoginLogService loginLogService;
@Override
public KeyboardUser selectUserWithSubjectId(String sub) {
return keyboardUserMapper.selectOne(
@@ -89,7 +91,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
}
@Override
public KeyboardUserRespVO login(UserLoginDTO userLoginDTO) {
public KeyboardUserRespVO login(UserLoginDTO userLoginDTO, HttpServletRequest request) {
KeyboardUser keyboardUser = keyboardUserMapper.selectOne(
new LambdaQueryWrapper<KeyboardUser>()
.eq(KeyboardUser::getEmail, userLoginDTO.getMail())
@@ -101,6 +103,46 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
throw new BusinessException(ErrorCode.PASSWORD_OR_MAIL_ERROR);
}
StpUtil.login(keyboardUser.getId());
// 记录登录日志
try {
String ipAddress = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
String platform = "Unknown";
String os = "Unknown";
if (userAgent != null) {
if (userAgent.contains("iOS")) {
platform = "iOS";
} else if (userAgent.contains("Android")) {
platform = "Android";
}
if (userAgent.contains("Windows")) {
os = "Windows";
} else if (userAgent.contains("Mac OS")) {
os = "Mac OS";
} else if (userAgent.contains("Linux")) {
os = "Linux";
} else if (userAgent.contains("iOS")) {
os = "iOS";
} else if (userAgent.contains("Android")) {
os = "Android";
}
}
loginLogService.recordLoginLog(
keyboardUser.getId(),
ipAddress,
userAgent,
os,
platform,
"SUCCESS"
);
} catch (Exception e) {
log.error("记录登录日志失败", e);
}
KeyboardUserRespVO keyboardUserRespVO = BeanUtil.copyProperties(keyboardUser, KeyboardUserRespVO.class);
keyboardUserRespVO.setToken(StpUtil.getTokenValue());
return keyboardUserRespVO;

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yolo.keyborad.mapper.KeyboardUserLoginLogMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardUserLoginLog">
<!--@mbg.generated-->
<!--@Table keyboard_user_login_log-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="login_time" jdbcType="TIMESTAMP" property="loginTime" />
<result column="ip_address" jdbcType="VARCHAR" property="ipAddress" />
<result column="device_info" jdbcType="VARCHAR" property="deviceInfo" />
<result column="os" jdbcType="VARCHAR" property="os" />
<result column="platform" jdbcType="VARCHAR" property="platform" />
<result column="status" jdbcType="VARCHAR" property="status" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, user_id, login_time, ip_address, device_info, os, platform, "status", created_at,
updated_at
</sql>
</mapper>