Initial commit
This commit is contained in:
354
后端架构设计/04-用户服务开发文档-part1.md
Normal file
354
后端架构设计/04-用户服务开发文档-part1.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# 用户服务开发文档
|
||||
|
||||
## 一、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;
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user