10 KiB
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;
}
}