SpringBoot电商系统用户注册、登录、留存及数据埋点设计与落地实施方案
一、方案整体概述
1.1 方案目标
本方案基于SpringBoot+MyBatis+Redis+MySQL技术栈,完成电商系统用户注册、账号登录、用户留存管理 核心业务开发,同时搭建标准化用户行为数据埋点体系。实现业务功能可稳定运行、用户行为数据可采集、可分析、可复盘,为电商用户运营、流失召回、产品迭代、流量转化优化提供数据支撑,兼顾业务可用性、数据准确性、系统高性能、可扩展性四大核心要求。
1.2 适用范围
适用于中小型电商Web/小程序/H5端用户体系搭建,包含新用户注册、账号密码登录、短信验证码登录、会话管理、用户留存统计(次日/7日/30日留存)、全链路用户行为埋点落地、数据上报与存储分析全流程。
1.3 技术栈选型
- 核心框架:SpringBoot 2.7.x、SpringMVC、MyBatis-Plus
- 数据库:MySQL 8.0(用户业务数据)
- 缓存中间件:Redis(验证码、登录Token、用户会话、留存临时统计数据)
- 数据埋点:自定义埋点SDK + 异步线程池上报 + Kafka(可选,高并发场景)
- 工具依赖:Hutool、Jackson、Validation、Redisson
- 数据统计:定时任务(Spring Task)+ 离线数据复盘
二、数据库表结构设计
围绕用户业务和埋点数据,设计3张核心数据表,分别承载用户基础信息、用户登录日志、用户行为埋点数据。
2.1 用户主表(user_info)
存储电商用户基础账号信息,支撑注册、登录核心业务
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text CREATE TABLE `user_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户主键ID', `username` varchar(32) DEFAULT '' COMMENT '用户名', `phone` varchar(11) NOT NULL COMMENT '手机号(唯一)', `password` varchar(64) NOT NULL COMMENT '加密密码(MD5+盐加密)', `salt` varchar(32) NOT NULL COMMENT '加密盐值', `nickname` varchar(32) DEFAULT '' COMMENT '用户昵称', `avatar` varchar(255) DEFAULT '' COMMENT '头像地址', `status` tinyint NOT NULL DEFAULT '1' COMMENT '账号状态 0-禁用 1-正常', `register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', `register_ip` varchar(64) DEFAULT '' COMMENT '注册IP', `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', `last_login_ip` varchar(64) DEFAULT '' COMMENT '最后登录IP', `del_flag` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记 0-正常 1-删除', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电商用户信息表'; |
2.2 用户登录日志表(user_login_log)
记录每一次登录行为,用于统计登录频次、异常登录、用户留存数据
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text CREATE TABLE `user_login_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '用户ID', `login_type` tinyint NOT NULL COMMENT '登录类型 1-账号密码 2-短信验证码', `login_ip` varchar(64) DEFAULT '', `login_device` varchar(64) DEFAULT '' COMMENT '登录设备', `login_status` tinyint NOT NULL COMMENT '登录状态 0-失败 1-成功', `login_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`,`login_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户登录日志表'; |
2.3 用户行为埋点表(user_track_log)
存储所有用户行为埋点数据,支撑注册、登录、浏览、留存等数据分析
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text CREATE TABLE `user_track_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT '0' COMMENT '用户ID,未登录为0', `track_event` varchar(64) NOT NULL COMMENT '埋点事件标识', `track_event_name` varchar(128) NOT NULL COMMENT '埋点事件名称', `track_params` json DEFAULT NULL COMMENT '事件扩展参数', `track_ip` varchar(64) DEFAULT '', `track_device` varchar(64) DEFAULT '', `track_source` varchar(32) DEFAULT '' COMMENT '来源端:H5/小程序/APP/PC', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_event_time` (`track_event`,`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为埋点日志表'; |
三、核心业务模块设计与落地
3.1 用户注册模块
3.1.1 业务流程
前端提交手机号、验证码、密码 → 后端校验手机号格式、验证码有效性(Redis校验)→ 校验手机号是否已注册 → 密码加盐加密 → 写入用户表 → 记录注册埋点事件 → 返回注册成功结果。
3.1.2 核心规则
- 手机号唯一,禁止重复注册;
- 短信验证码5分钟有效期,Redis存储key:sms:code:{phone};
- 密码采用MD5+随机盐加密存储,明文不落地;
- 注册成功自动生成用户昵称,默认无头像;
- 注册完成同步上报【用户注册成功】埋点。
3.1.3 核心代码实现
- 注册接口Controller
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @RestController @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserService userService; @Autowired private TrackLogService trackLogService; @PostMapping("/register") public Result register(@RequestBody UserRegisterDTO dto) { // 参数校验 ValidatorUtil.validate(dto); // 执行注册逻辑 UserInfo user = userService.register(dto); // 注册埋点上报 trackLogService.trackRegisterEvent(user, dto.getTrackSource()); return Result.success("注册成功"); } } |
- 注册业务Service
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @Service public class UserServiceImpl implements UserService { @Autowired private UserInfoMapper userInfoMapper; @Autowired private RedisTemplate<String, String> redisTemplate; @Override @Transactional(rollbackFor = Exception.class) public UserInfo register(UserRegisterDTO dto) { String phone = dto.getPhone(); // 1. 校验验证码 String codeKey = "sms:code:" + phone; String redisCode = redisTemplate.opsForValue().get(codeKey); if (StrUtil.isBlank(redisCode) || !redisCode.equals(dto.getCode())) { throw new BusinessException("验证码错误或已过期"); } // 2. 校验手机号是否已注册 LambdaQueryChainWrapper<UserInfo> query = new LambdaQueryChainWrapper<>(userInfoMapper); UserInfo existUser = query.eq(UserInfo::getPhone, phone).one(); if (Objects.nonNull(existUser)) { throw new BusinessException("手机号已注册"); } // 3. 密码加密 String salt = RandomUtil.randomString(16); String password = DigestUtil.md5Hex(dto.getPassword() + salt); // 4. 组装用户数据 UserInfo user = new UserInfo(); user.setPhone(phone); user.setPassword(password); user.setSalt(salt); user.setNickname("用户" + phone.substring(7)); user.setRegisterIp(IpUtil.getIpAddr(ServletUtil.getRequest())); user.setStatus(1); user.setDelFlag(0); user.setRegisterTime(LocalDateTime.now()); // 5. 插入数据库 userInfoMapper.insert(user); // 6. 删除验证码 redisTemplate.delete(codeKey); return user; } } |
3.2 用户登录模块
3.2.1 业务流程
支持账号密码登录、短信验证码登录两种方式,前端提交参数 → 后端校验参数合法性 → 校验账号状态 → 验证密码/验证码 → 生成JWT/Redis Token → 记录登录日志 → 上报登录埋点 → 返回Token和用户信息。
3.2.2 核心规则
- 禁用账号禁止登录,返回账号异常提示;
- 登录Token有效期2小时,Redis缓存用户会话;
- 单次登录失败记录日志,多次失败触发风控拦截;
- 登录成功更新用户最后登录时间、IP;
- 区分登录类型上报不同埋点事件。
3.2.3 核心代码实现
登录核心Service逻辑
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @Override @Transactional(rollbackFor = Exception.class) public UserLoginVO login(UserLoginDTO dto) { String phone = dto.getPhone(); // 查询用户 UserInfo user = new LambdaQueryChainWrapper<>(userInfoMapper) .eq(UserInfo::getPhone, phone) .eq(UserInfo::getDelFlag, 0) .one(); if (Objects.isNull(user)) { throw new BusinessException("用户不存在"); } if (user.getStatus() == 0) { throw new BusinessException("账号已被禁用"); } boolean loginSuccess = false; // 账号密码登录 if (dto.getLoginType() == 1) { String encryptPwd = DigestUtil.md5Hex(dto.getPassword() + user.getSalt()); if (!user.getPassword().equals(encryptPwd)) { // 记录登录失败日志 recordLoginLog(user.getId(), 1, 0); throw new BusinessException("密码错误"); } loginSuccess = true; } else { // 短信验证码登录 String codeKey = "sms:code:" + phone; String redisCode = redisTemplate.opsForValue().get(codeKey); if (StrUtil.isBlank(redisCode) || !redisCode.equals(dto.getCode())) { recordLoginLog(user.getId(), 2, 0); throw new BusinessException("验证码错误或过期"); } loginSuccess = true; redisTemplate.delete(codeKey); } // 登录成功处理 if (loginSuccess) { // 生成Token String token = JwtUtil.generateToken(user.getId().toString()); // 缓存用户会话 redisTemplate.opsForValue().set("user:token:" + token, JSONUtil.toJsonStr(user), 2, TimeUnit.HOURS); // 更新用户登录信息 user.setLastLoginTime(LocalDateTime.now()); user.setLastLoginIp(IpUtil.getIpAddr(ServletUtil.getRequest())); userInfoMapper.updateById(user); // 记录登录日志 recordLoginLog(user.getId(), dto.getLoginType(), 1); // 上报登录埋点 trackLogService.trackLoginEvent(user, dto.getLoginType(), dto.getTrackSource()); // 组装返回数据 UserLoginVO vo = new UserLoginVO(); vo.setToken(token); vo.setUserId(user.getId()); vo.setNickname(user.getNickname()); return vo; } return null; } |
3.3 用户留存统计模块
3.3.1 留存定义
- 次日留存:当日注册用户,次日再次登录/访问系统;
- 7日留存:当日注册用户,注册后第7日再次活跃;
- 30日留存:当日注册用户,注册后第30日再次活跃;
- 活跃判定标准:用户登录系统、触发任意核心行为埋点。
3.3.2 实现方案
采用定时任务离线统计,每日凌晨2点执行留存统计任务,统计前一日、近7日、近30日注册用户的活跃情况,数据落地留存统计表(可自建user_retention表)。
3.3.3 定时任务核心代码
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @Component @EnableScheduling @Slf4j public class UserRetentionTask { @Autowired private UserInfoMapper userInfoMapper; @Autowired private UserLoginLogMapper loginLogMapper; // 每日凌晨2点执行 @Scheduled(cron = "0 0 2 * * ?") public void calcUserRetention() { log.info("开始执行用户留存统计任务"); // 1. 统计次日留存 calcNextDayRetention(); // 2. 统计7日留存 calcSevenDayRetention(); // 3. 统计30日留存 calcThirtyDayRetention(); log.info("用户留存统计任务执行完成"); } // 次日留存统计逻辑 private void calcNextDayRetention() { // 获取昨日注册用户 LocalDate yesterday = LocalDate.now().minusDays(1); List<UserInfo> registerUserList = userInfoMapper.selectRegisterUserByDate(yesterday); if (CollUtil.isEmpty(registerUserList)) { return; } // 筛选昨日注册、今日活跃的用户 LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); int retainCount = 0; for (UserInfo user : registerUserList) { Integer activeCount = loginLogMapper.countUserLogin(user.getId(), todayStart, LocalDateTime.now()); if (activeCount > 0) { retainCount++; } } // 计算留存率、存入留存统计表 double retentionRate = BigDecimalUtil.div(retainCount, registerUserList.size(), 4).doubleValue(); // 数据入库逻辑省略 } } |
四、数据埋点体系整体设计
4.1 埋点核心原则
- 无侵入性:通过AOP+异步线程池实现埋点上报,不影响主业务响应速度;
- 全链路覆盖:覆盖注册、登录、页面访问、功能点击、退出、留存活跃全场景;
- 数据标准化:统一埋点字段、事件标识、参数格式,便于后续数据分析;
- 高性能异步:埋点数据异步上报、异步入库,避免同步阻塞业务接口。
4.2 核心埋点事件清单(用户体系)
|-----------------------|--------|---------------------|-----------------------|
| 事件标识 | 事件名称 | 埋点时机 | 核心参数 |
| user_register_success | 用户注册成功 | 注册接口执行成功后 | 手机号、注册时间、注册IP、设备、来源端 |
| user_register_fail | 用户注册失败 | 注册参数错误/验证码失效/手机号已注册 | 失败原因、手机号、设备、来源端 |
| user_login_success | 用户登录成功 | 登录校验通过、会话创建成功 | 用户ID、登录类型、登录IP、设备、来源端 |
| user_login_fail | 用户登录失败 | 密码错误/验证码失效/账号禁用 | 用户ID、失败原因、登录类型、设备 |
| user_active | 用户日常活跃 | 用户每日首次访问系统/操作核心功能 | 用户ID、活跃时间、访问页面、设备 |
| user_logout | 用户退出登录 | 用户主动退出/会话过期退出 | 用户ID、退出时间、在线时长、设备 |
4.3 埋点技术架构(AOP+异步上报)
4.3.1 架构流程
业务接口执行 → 自定义注解标记埋点事件 → AOP切面拦截请求 → 封装埋点数据 → 异步线程池上报 → 批量入库/推送Kafka → 数据分析。
4.3.2 自定义埋点注解
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TrackEvent { // 埋点事件标识 String event(); // 事件名称 String eventName(); } |
4.3.3 AOP切面核心代码
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text @Aspect @Component @Slf4j public class TrackEventAspect { @Autowired private TrackLogService trackLogService; // 自定义异步线程池 @Autowired private ThreadPoolExecutor trackThreadPool; @Pointcut("@annotation(com.ec.annotation.TrackEvent)") public void trackPointCut(){} @AfterReturning("trackPointCut() && @annotation(trackEvent)") public void doTrack(JoinPoint joinPoint, TrackEvent trackEvent) { // 异步执行埋点上报,不阻塞主线程 trackThreadPool.execute(() -> { try { // 封装请求信息、用户信息、事件参数 TrackLogDTO dto = new TrackLogDTO(); dto.setTrackEvent(trackEvent.event()); dto.setTrackEventName(trackEvent.eventName()); dto.setTrackIp(IpUtil.getIpAddr(ServletUtil.getRequest())); dto.setTrackDevice(DeviceUtil.getDeviceInfo(ServletUtil.getRequest())); // 填充用户ID、扩展参数 trackLogService.buildTrackParams(dto, joinPoint); // 入库上报 trackLogService.saveTrackLog(dto); } catch (Exception e) { log.error("埋点上报异常", e); } }); } } |
五、落地实施全流程
5.1 环境与依赖准备
- 初始化SpringBoot项目,引入MyBatis-Plus、Redis、Validation、AOP、Hutool依赖;
- 执行上述数据库SQL脚本,创建3张核心数据表;
- 配置Redis连接、数据库连接、异步线程池参数;
- 封装统一返回结果、全局异常处理器、工具类(JWT、IP、加密工具)。
5.2 业务功能落地步骤
- 搭建用户模块实体类、DTO、Mapper、Service、Controller分层结构;
- 开发短信验证码发送、Redis缓存校验功能;
- 实现用户注册、双模式登录、退出登录核心业务;
- 开发登录日志记录、用户会话管理功能;
- 配置Spring Task定时任务,实现用户留存数据统计。
5.3 埋点功能落地步骤
- 创建自定义@TrackEvent埋点注解、AOP切面类;
- 封装埋点数据DTO、异步上报工具类;
- 在注册、登录、活跃、退出接口添加对应埋点注解;
- 实现埋点数据入库逻辑,高并发场景接入Kafka消息队列削峰;
- 验证埋点数据完整性、准确性,排查漏报、错报问题。
5.4 测试验证
- 功能测试:测试注册成功/失败、登录成功/失败、账号禁用、验证码失效等场景;
- 埋点测试:触发各类用户行为,校验user_track_log表数据完整;
- 留存测试:模拟用户注册、次日登录,验证留存统计任务数据准确;
- 性能测试:压测接口,验证异步埋点不影响业务响应性能。
5.5 线上运维
- 埋点日志单独归档,定期清理过期历史数据,避免数据表臃肿;
- 监控定时任务执行状态,异常告警;
- 基于埋点数据生成用户留存报表、注册转化率、登录失败率分析报表;
- 根据数据优化注册流程、登录风控、用户召回策略。
六、优化与扩容方案
6.1 性能优化
- 埋点数据采用批量入库策略,替代单条入库,提升吞吐量;
- 高频埋点事件通过Kafka异步削峰,缓解数据库压力;
- Redis缓存热门用户会话数据,减少数据库查询频次。
6.2 业务扩容
- 可扩展第三方登录(微信、支付宝、QQ)埋点统计;
- 新增用户流失埋点、复购埋点、会员行为埋点;
- 对接大数据平台,实现用户画像、精准运营、流失预警。
6.3 安全优化
- 登录接口添加限流风控,防止暴力破解;
- 敏感埋点数据脱敏存储,保护用户隐私;
- 注册接口防刷,限制同一IP高频注册请求。
1、自定义埋点异步线程池配置 TrackThreadPoolConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 埋点专用异步线程池,和业务线程池隔离,埋点异常不影响主业务
*/
@Configuration
public class TrackThreadPoolConfig {
@Bean("trackThreadPool")
public Executor trackThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数
executor.setCorePoolSize(8);
//最大线程
executor.setMaxPoolSize(16);
//队列容量
executor.setQueueCapacity(2000);
//空闲线程存活时间
executor.setKeepAliveSeconds(60);
//线程前缀
executor.setThreadNamePrefix("track-log-thread-");
//拒绝策略:队列满了,直接丢弃埋点任务,不阻塞业务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
executor.initialize();
return executor;
}
}
2、全局业务异常 BusinessException
/**
* 业务自定义异常
*/
public class BusinessException extends RuntimeException{
private Integer code;
private String msg;
public BusinessException(String msg){
super(msg);
this.code = 500;
this.msg = msg;
}
public BusinessException(Integer code,String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
3、统一返回对象 Result
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(){
Result<T> r = new Result<>();
r.setCode(200);
r.setMsg("success");
return r;
}
public static <T> Result<T> success(String msg){
Result<T> r = new Result<>();
r.setCode(200);
r.setMsg(msg);
return r;
}
public static <T> Result<T> success(T data){
Result<T> r = new Result<>();
r.setCode(200);
r.setMsg("success");
r.setData(data);
return r;
}
public static <T> Result<T> fail(String msg){
Result<T> r = new Result<>();
r.setCode(500);
r.setMsg(msg);
return r;
}
public static <T> Result<T> fail(Integer code,String msg){
Result<T> r = new Result<>();
r.setCode(code);
r.setMsg(msg);
return r;
}
}
4、全局异常处理器 GlobalExceptionHandler
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<?> businessExceptionHandler(BusinessException e){
log.warn("业务异常:{}",e.getMsg());
return Result.fail(e.getCode(),e.getMsg());
}
/**
* 参数校验异常 @Valid
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> validException(MethodArgumentNotValidException e){
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(msg);
}
@ExceptionHandler(BindException.class)
public Result<?> bindException(BindException e){
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(msg);
}
/**
* 系统未知异常
*/
@ExceptionHandler(Exception.class)
public Result<?> exceptionHandler(Exception e){
log.error("系统未知异常",e);
return Result.fail("系统繁忙,请稍后重试");
}
}
5、DTO 对象(注册、登录、埋点 DTO)
UserRegisterDTO
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
public class UserRegisterDTO {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$",message = "手机号格式错误")
private String phone;
@NotBlank(message = "验证码不能为空")
private String code;
@NotBlank(message = "密码不能为空")
private String password;
/**来源端 H5 /小程序 /APP /PC*/
private String trackSource;
}
UserLoginDTO
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class UserLoginDTO {
@NotBlank(message = "手机号不能为空")
private String phone;
/**登录类型 1账号密码 2短信验证码*/
private Integer loginType;
private String password;
private String code;
private String trackSource;
}
UserLoginVO
import lombok.Data;
@Data
public class UserLoginVO {
private String token;
private Long userId;
private String nickname;
}
TrackLogDTO 埋点 DTO
import lombok.Data;
import java.util.Map;
@Data
public class TrackLogDTO {
private Long userId;
/**事件编码 user_register_success*/
private String trackEvent;
/**事件名称*/
private String trackEventName;
/**扩展参数*/
private Map<String,Object> trackParams;
private String trackIp;
private String trackDevice;
private String trackSource;
}
6、TrackEvent 埋点注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TrackEvent {
String event();
String eventName();
}
7、TrackLogService 埋点服务接口 + 实现
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.aspectj.lang.JoinPoint;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class TrackLogServiceImpl extends ServiceImpl<UserTrackLogMapper, UserTrackLog> implements TrackLogService {
@Override
public void trackRegisterEvent(UserInfo user,String source){
TrackLogDTO dto = new TrackLogDTO();
dto.setUserId(user.getId());
dto.setTrackEvent("user_register_success");
dto.setTrackEventName("用户注册成功");
dto.setTrackSource(source);
Map<String,Object> params = new HashMap<>();
params.put("phone",user.getPhone());
params.put("registerIp",user.getRegisterIp());
dto.setTrackParams(params);
saveTrackLog(dto);
}
@Override
public void trackLoginEvent(UserInfo user,Integer loginType,String source){
TrackLogDTO dto = new TrackLogDTO();
dto.setUserId(user.getId());
dto.setTrackEvent("user_login_success");
dto.setTrackEventName("用户登录成功");
dto.setTrackSource(source);
Map<String,Object> params = new HashMap<>();
params.put("loginType",loginType);
dto.setTrackParams(params);
saveTrackLog(dto);
}
@Override
public void saveTrackLog(TrackLogDTO dto) {
try {
UserTrackLog log = new UserTrackLog();
log.setUserId(dto.getUserId()==null?0:dto.getUserId());
log.setTrackEvent(dto.getTrackEvent());
log.setTrackEventName(dto.getTrackEventName());
log.setTrackIp(dto.getTrackIp());
log.setTrackDevice(dto.getTrackDevice());
log.setTrackSource(dto.getTrackSource());
if(dto.getTrackParams()!=null){
log.setTrackParams(com.alibaba.fastjson.JSON.toJSONString(dto.getTrackParams()));
}
baseMapper.insert(log);
}catch (Exception e){
log.error("埋点入库异常",e);
}
}
@Override
public void buildTrackParams(TrackLogDTO dto, JoinPoint joinPoint) {
//可以在这里解析方法入参,填充扩展参数
}
}
8、application.yml 关键配置片段
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ec_user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
#开启定时任务
task:
scheduling:
pool:
size: 5
使用示例:注解埋点用法
在 controller 方法上直接加注解,AOP 自动采集埋点,不侵入业务代码
@PostMapping("/register")
@TrackEvent(event = "user_register_success",eventName = "用户注册成功")
public Result register(@RequestBody @Valid UserRegisterDTO dto) {
UserInfo user = userService.register(dto);
return Result.success("注册成功");
}