Files
number/后端架构设计/04-用户服务开发文档-part1.md
2026-03-17 12:09:43 +08:00

10 KiB

用户服务开发文档

一、Entity 实体类

User.java

package com.openclaw.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("users")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String phone;
    private String passwordHash;
    private String nickname;
    private String avatarUrl;
    private String status;       // active / inactive / banned
    private String memberLevel;  // normal / silver / gold / diamond
    private Integer growthValue;
    private String banReason;    // 封禁原因
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    @TableLogic
    private LocalDateTime deletedAt;
}

UserProfile.java

package com.openclaw.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("user_profiles")
public class UserProfile {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private String realName;
    private String idCard;
    private String gender;      // male / female / unknown
    private LocalDate birthday;
    private String city;
    private String bio;
    private String authStatus;  // none / pending / approved / rejected
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

二、DTO / VO

UserRegisterDTO.java

package com.openclaw.dto;

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

@Data
public class UserRegisterDTO {
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度6-20位")
    private String password;

    @NotBlank(message = "验证码不能为空")
    private String smsCode;

    private String inviteCode;  // 邀请码(可选)
}

UserLoginDTO.java

package com.openclaw.dto;

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

@Data
public class UserLoginDTO {
    @NotBlank(message = "手机号不能为空")
    private String phone;

    @NotBlank(message = "密码不能为空")
    private String password;
}

UserUpdateDTO.java

package com.openclaw.dto;

import lombok.Data;
import java.time.LocalDate;

@Data
public class UserUpdateDTO {
    private String nickname;
    private String avatarUrl;   // 腾讯云COS上传后的URL
    private String gender;
    private LocalDate birthday;
    private String city;
    private String bio;
}

UserVO.java

package com.openclaw.vo;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class UserVO {
    private Long id;
    private String phone;
    private String nickname;
    private String avatarUrl;
    private String memberLevel;
    private Integer growthValue;
    private Integer availablePoints;
    private String inviteCode;
    private LocalDateTime createdAt;
}

LoginVO.java

package com.openclaw.vo;

import lombok.Data;

@Data
public class LoginVO {
    private String token;
    private UserVO user;
}

三、Service 接口

UserService.java

package com.openclaw.service;

import com.openclaw.dto.*;
import com.openclaw.vo.*;

public interface UserService {
    void sendSmsCode(String phone);
    LoginVO register(UserRegisterDTO dto);
    LoginVO login(UserLoginDTO dto);
    void logout(String token);
    UserVO getCurrentUser(Long userId);
    UserVO updateProfile(Long userId, UserUpdateDTO dto);
    void changePassword(Long userId, String oldPassword, String newPassword);
    void resetPassword(String phone, String smsCode, String newPassword);
}

四、Service 实现(核心逻辑)

UserServiceImpl.java

package com.openclaw.service.impl;

import com.openclaw.constant.ErrorCode;
import com.openclaw.dto.*;
import com.openclaw.entity.*;
import com.openclaw.exception.BusinessException;
import com.openclaw.repository.*;
import com.openclaw.service.*;
import com.openclaw.util.JwtUtil;
import com.openclaw.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final UserProfileRepository userProfileRepository;
    private final UserPointsRepository userPointsRepository;
    private final InviteCodeRepository inviteCodeRepository;
    private final PointsService pointsService;
    private final InviteService inviteService;
    private final PasswordEncoder passwordEncoder;
    private final StringRedisTemplate redisTemplate;
    private final JwtUtil jwtUtil;

    @Override
    public void sendSmsCode(String phone) {
        String code = String.valueOf((int)((Math.random() * 9 + 1) * 100000));
        redisTemplate.opsForValue().set("captcha:sms:" + phone, code, 5, TimeUnit.MINUTES);
        // TODO: 调用腾讯云短信SDK发送
    }

    @Override
    @Transactional
    public LoginVO register(UserRegisterDTO dto) {
        // 1. 校验短信验证码
        String cached = redisTemplate.opsForValue().get("captcha:sms:" + dto.getPhone());
        if (!dto.getSmsCode().equals(cached)) throw new BusinessException(ErrorCode.SMS_CODE_ERROR);

        // 2. 手机号唯一性检查
        if (userRepository.existsByPhone(dto.getPhone())) throw new BusinessException(ErrorCode.PHONE_ALREADY_EXISTS);

        // 3. 创建用户
        User user = new User();
        user.setPhone(dto.getPhone());
        user.setPasswordHash(passwordEncoder.encode(dto.getPassword()));
        user.setNickname("用户" + dto.getPhone().substring(7));
        user.setStatus("active");
        user.setMemberLevel("normal");
        user.setGrowthValue(0);
        userRepository.save(user);

        // 4. 初始化资料
        UserProfile profile = new UserProfile();
        profile.setUserId(user.getId());
        profile.setAuthStatus("none");
        userProfileRepository.save(profile);

        // 5. 初始化积分 + 注册奖励
        pointsService.initUserPoints(user.getId());
        pointsService.earnPoints(user.getId(), "register", null, null);

        // 6. 邀请码处理
        if (dto.getInviteCode() != null) {
            inviteService.handleInviteRegister(dto.getInviteCode(), user.getId());
        }

        // 7. 生成自己的邀请码
        inviteService.generateInviteCode(user.getId());

        // 8. 清除验证码
        redisTemplate.delete("captcha:sms:" + dto.getPhone());

        return buildLoginVO(user);
    }

    @Override
    public LoginVO login(UserLoginDTO dto) {
        User user = userRepository.findByPhone(dto.getPhone())
            .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
        if ("banned".equals(user.getStatus())) throw new BusinessException(ErrorCode.USER_BANNED);
        if (!passwordEncoder.matches(dto.getPassword(), user.getPasswordHash()))
            throw new BusinessException(ErrorCode.PASSWORD_ERROR);
        return buildLoginVO(user);
    }

    @Override
    public void logout(String token) {
        long remaining = jwtUtil.getExpiration(token);
        redisTemplate.opsForValue().set("user:token:" + token, "1", remaining, TimeUnit.SECONDS);
    }

    @Override
    public UserVO getCurrentUser(Long userId) {
        User user = userRepository.getById(userId);
        if (user == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
        return buildUserVO(user);
    }

    @Override
    @Transactional
    public UserVO updateProfile(Long userId, UserUpdateDTO dto) {
        User user = userRepository.getById(userId);
        if (dto.getNickname() != null) user.setNickname(dto.getNickname());
        if (dto.getAvatarUrl() != null) user.setAvatarUrl(dto.getAvatarUrl());
        userRepository.updateById(user);

        UserProfile p = userProfileRepository.findByUserId(userId);
        if (dto.getGender() != null) p.setGender(dto.getGender());
        if (dto.getBirthday() != null) p.setBirthday(dto.getBirthday());
        if (dto.getCity() != null) p.setCity(dto.getCity());
        if (dto.getBio() != null) p.setBio(dto.getBio());
        userProfileRepository.updateById(p);
        return buildUserVO(user);
    }

    @Override
    public void changePassword(Long userId, String oldPwd, String newPwd) {
        User user = userRepository.getById(userId);
        if (!passwordEncoder.matches(oldPwd, user.getPasswordHash()))
            throw new BusinessException(ErrorCode.PASSWORD_ERROR);
        user.setPasswordHash(passwordEncoder.encode(newPwd));
        userRepository.updateById(user);
    }

    @Override
    public void resetPassword(String phone, String smsCode, String newPassword) {
        String cached = redisTemplate.opsForValue().get("captcha:sms:" + phone);
        if (!smsCode.equals(cached)) throw new BusinessException(ErrorCode.SMS_CODE_ERROR);
        User user = userRepository.findByPhone(phone)
            .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
        user.setPasswordHash(passwordEncoder.encode(newPassword));
        userRepository.updateById(user);
        redisTemplate.delete("captcha:sms:" + phone);
    }

    private LoginVO buildLoginVO(User user) {
        LoginVO vo = new LoginVO();
        vo.setToken(jwtUtil.generateToken(user.getId()));
        vo.setUser(buildUserVO(user));
        return vo;
    }

    private UserVO buildUserVO(User user) {
        UserVO vo = new UserVO();
        vo.setId(user.getId());
        vo.setPhone(user.getPhone());
        vo.setNickname(user.getNickname());
        vo.setAvatarUrl(user.getAvatarUrl());
        vo.setMemberLevel(user.getMemberLevel());
        vo.setGrowthValue(user.getGrowthValue());
        vo.setCreatedAt(user.getCreatedAt());
        UserPoints pts = userPointsRepository.findByUserId(user.getId());
        if (pts != null) vo.setAvailablePoints(pts.getAvailablePoints());
        InviteCode ic = inviteCodeRepository.findByUserId(user.getId());
        if (ic != null) vo.setInviteCode(ic.getCode());
        return vo;
    }
}