# 用户服务开发文档 ## 一、Entity 实体类 ### User.java ```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 ```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 ```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 ```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 ```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 ```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 ```java package com.openclaw.vo; import lombok.Data; @Data public class LoginVO { private String token; private UserVO user; } ``` ## 三、Service 接口 ### UserService.java ```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 ```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; } } ```