SpringBoot电商系统用户注册、登录、留存及数据埋点设计与落地实施方案

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 核心代码实现

  1. 注册接口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("注册成功"); } } |

  1. 注册业务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 环境与依赖准备

  1. 初始化SpringBoot项目,引入MyBatis-Plus、Redis、Validation、AOP、Hutool依赖;
  1. 执行上述数据库SQL脚本,创建3张核心数据表;
  1. 配置Redis连接、数据库连接、异步线程池参数;
  1. 封装统一返回结果、全局异常处理器、工具类(JWT、IP、加密工具)。

5.2 业务功能落地步骤

  1. 搭建用户模块实体类、DTO、Mapper、Service、Controller分层结构;
  1. 开发短信验证码发送、Redis缓存校验功能;
  1. 实现用户注册、双模式登录、退出登录核心业务;
  1. 开发登录日志记录、用户会话管理功能;
  1. 配置Spring Task定时任务,实现用户留存数据统计。

5.3 埋点功能落地步骤

  1. 创建自定义@TrackEvent埋点注解、AOP切面类;
  1. 封装埋点数据DTO、异步上报工具类;
  1. 在注册、登录、活跃、退出接口添加对应埋点注解;
  1. 实现埋点数据入库逻辑,高并发场景接入Kafka消息队列削峰;
  1. 验证埋点数据完整性、准确性,排查漏报、错报问题。

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("注册成功");
}
相关推荐
摇滚侠39 分钟前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 数据库初始化机制 阅读笔记 36
数据库·spring boot·笔记
qiuhaipeng11 小时前
Claude code 升级后上下文长度变短问题
java·前端·数据库
wxwx_bscxy3222 小时前
基于Python新冠疫情可视化分析系统的设计与实现
java·数据库·spring boot·python·微信小程序·sqlite
evans在进步3 小时前
Tomcat NIO 工作原理详解:从 Acceptor、Poller 到容器处理链
java·tomcat·nio
IT_陈寒4 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端
渡我白衣4 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
电商API_180079052474 小时前
电商商品价格监控工具淘宝京东商品价格抓取API项目实操分享
java·大数据·开发语言·前端·数据挖掘
zzzll11114 小时前
深度剖析:HashMap 并发死循环与 ConcurrentHashMap 两代演进全解
java·开发语言
秋名RG4 小时前
2026/5/27 生产事故复盘与整改方案
java