Initial commit
This commit is contained in:
374
后端架构设计/04-用户服务开发文档-part2.md
Normal file
374
后端架构设计/04-用户服务开发文档-part2.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# 用户服务开发文档 - Part 2(Controller + 通用工具)
|
||||
|
||||
## 五、Controller
|
||||
|
||||
### UserController.java
|
||||
|
||||
```java
|
||||
package com.openclaw.controller;
|
||||
|
||||
import com.openclaw.common.Result;
|
||||
import com.openclaw.dto.*;
|
||||
import com.openclaw.service.UserService;
|
||||
import com.openclaw.util.UserContext;
|
||||
import com.openclaw.vo.*;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/** 发送短信验证码(注册/找回密码用) */
|
||||
@PostMapping("/sms-code")
|
||||
public Result<Void> sendSmsCode(@RequestParam String phone) {
|
||||
userService.sendSmsCode(phone);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 用户注册 */
|
||||
@PostMapping("/register")
|
||||
public Result<LoginVO> register(@Valid @RequestBody UserRegisterDTO dto) {
|
||||
return Result.ok(userService.register(dto));
|
||||
}
|
||||
|
||||
/** 用户登录 */
|
||||
@PostMapping("/login")
|
||||
public Result<LoginVO> login(@Valid @RequestBody UserLoginDTO dto) {
|
||||
return Result.ok(userService.login(dto));
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(@RequestHeader("Authorization") String authorization) {
|
||||
String token = authorization.replace("Bearer ", "");
|
||||
userService.logout(token);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 获取当前用户信息 */
|
||||
@GetMapping("/profile")
|
||||
public Result<UserVO> getProfile() {
|
||||
return Result.ok(userService.getCurrentUser(UserContext.getUserId()));
|
||||
}
|
||||
|
||||
/** 更新个人信息 */
|
||||
@PutMapping("/profile")
|
||||
public Result<UserVO> updateProfile(@RequestBody UserUpdateDTO dto) {
|
||||
return Result.ok(userService.updateProfile(UserContext.getUserId(), dto));
|
||||
}
|
||||
|
||||
/** 修改密码 */
|
||||
@PutMapping("/password")
|
||||
public Result<Void> changePassword(
|
||||
@RequestParam String oldPassword,
|
||||
@RequestParam String newPassword) {
|
||||
userService.changePassword(UserContext.getUserId(), oldPassword, newPassword);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 忘记密码 - 重置 */
|
||||
@PostMapping("/password/reset")
|
||||
public Result<Void> resetPassword(
|
||||
@RequestParam String phone,
|
||||
@RequestParam String smsCode,
|
||||
@RequestParam String newPassword) {
|
||||
userService.resetPassword(phone, smsCode, newPassword);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 六、通用工具类
|
||||
|
||||
### Result.java
|
||||
|
||||
```java
|
||||
package com.openclaw.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Result<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
private Long timestamp = System.currentTimeMillis();
|
||||
|
||||
public static <T> Result<T> ok(T data) {
|
||||
Result<T> r = new Result<>();
|
||||
r.setCode(200);
|
||||
r.setMessage("success");
|
||||
r.setData(data);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok() { return ok(null); }
|
||||
|
||||
public static <T> Result<T> error(int code, String message) {
|
||||
Result<T> r = new Result<>();
|
||||
r.setCode(code);
|
||||
r.setMessage(message);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ErrorCode.java
|
||||
|
||||
```java
|
||||
package com.openclaw.constant;
|
||||
|
||||
public interface ErrorCode {
|
||||
// 用户模块 1xxx
|
||||
BusinessError USER_NOT_FOUND = new BusinessError(1001, "用户不存在");
|
||||
BusinessError PASSWORD_ERROR = new BusinessError(1002, "密码错误");
|
||||
BusinessError PHONE_ALREADY_EXISTS = new BusinessError(1003, "手机号已注册");
|
||||
BusinessError USER_BANNED = new BusinessError(1004, "账号已封禁");
|
||||
BusinessError SMS_CODE_ERROR = new BusinessError(1005, "验证码错误或已过期");
|
||||
|
||||
// Skill模块 2xxx
|
||||
BusinessError SKILL_NOT_FOUND = new BusinessError(2001, "Skill不存在");
|
||||
BusinessError SKILL_OFFLINE = new BusinessError(2002, "Skill已下架");
|
||||
BusinessError SKILL_ALREADY_OWNED = new BusinessError(2003, "已拥有该Skill");
|
||||
|
||||
// 积分模块 3xxx
|
||||
BusinessError POINTS_NOT_ENOUGH = new BusinessError(3001, "积分不足");
|
||||
BusinessError ALREADY_SIGNED_IN = new BusinessError(3002, "今日已签到");
|
||||
|
||||
// 订单模块 4xxx
|
||||
BusinessError ORDER_NOT_FOUND = new BusinessError(4001, "订单不存在");
|
||||
BusinessError ORDER_STATUS_ERROR = new BusinessError(4002, "订单状态异常");
|
||||
|
||||
// 支付模块 5xxx
|
||||
BusinessError PAYMENT_FAILED = new BusinessError(5001, "支付失败");
|
||||
BusinessError RECHARGE_NOT_FOUND = new BusinessError(5002, "充值订单不存在");
|
||||
|
||||
record BusinessError(int code, String message) {}
|
||||
}
|
||||
```
|
||||
|
||||
### BusinessException.java
|
||||
|
||||
```java
|
||||
package com.openclaw.exception;
|
||||
|
||||
import com.openclaw.constant.ErrorCode.BusinessError;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
private final int code;
|
||||
|
||||
public BusinessException(BusinessError error) {
|
||||
super(error.message());
|
||||
this.code = error.code();
|
||||
}
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GlobalExceptionHandler.java
|
||||
|
||||
```java
|
||||
package com.openclaw.exception;
|
||||
|
||||
import com.openclaw.common.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Void> handleBusiness(BusinessException e) {
|
||||
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<Void> handleValidation(BindException e) {
|
||||
String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
||||
return Result.error(400, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
return Result.error(500, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JwtUtil.java
|
||||
|
||||
```java
|
||||
package com.openclaw.util;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:604800}")
|
||||
private long expiration; // 默认7天(秒)
|
||||
|
||||
private Key getKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
|
||||
public String generateToken(Long userId) {
|
||||
return Jwts.builder()
|
||||
.setSubject(String.valueOf(userId))
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
|
||||
.signWith(getKey(), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Long getUserId(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getKey()).build()
|
||||
.parseClaimsJws(token).getBody();
|
||||
return Long.parseLong(claims.getSubject());
|
||||
}
|
||||
|
||||
public long getExpiration(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getKey()).build()
|
||||
.parseClaimsJws(token).getBody();
|
||||
long now = System.currentTimeMillis();
|
||||
return (claims.getExpiration().getTime() - now) / 1000;
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
try {
|
||||
Jwts.parserBuilder().setSigningKey(getKey()).build().parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (JwtException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AuthInterceptor.java(JWT认证拦截器)
|
||||
|
||||
```java
|
||||
package com.openclaw.interceptor;
|
||||
|
||||
import com.openclaw.util.JwtUtil;
|
||||
import com.openclaw.util.UserContext;
|
||||
import jakarta.servlet.http.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
|
||||
String auth = req.getHeader("Authorization");
|
||||
if (!StringUtils.hasText(auth) || !auth.startsWith("Bearer ")) {
|
||||
res.setStatus(401);
|
||||
return false;
|
||||
}
|
||||
String token = auth.substring(7);
|
||||
// 检查 Token 是否在黑名单(已登出)
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey("user:token:" + token))) {
|
||||
res.setStatus(401);
|
||||
return false;
|
||||
}
|
||||
if (!jwtUtil.isValid(token)) {
|
||||
res.setStatus(401);
|
||||
return false;
|
||||
}
|
||||
UserContext.setUserId(jwtUtil.getUserId(token));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object h, Exception ex) {
|
||||
UserContext.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UserContext.java
|
||||
|
||||
```java
|
||||
package com.openclaw.util;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
|
||||
|
||||
public static void setUserId(Long userId) { USER_ID.set(userId); }
|
||||
public static Long getUserId() { return USER_ID.get(); }
|
||||
public static void clear() { USER_ID.remove(); }
|
||||
}
|
||||
```
|
||||
|
||||
### WebMvcConfig.java(注册拦截器)
|
||||
|
||||
```java
|
||||
package com.openclaw.config;
|
||||
|
||||
import com.openclaw.interceptor.AuthInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.*;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
// 不需要登录的接口
|
||||
.excludePathPatterns(
|
||||
"/api/v1/users/sms-code",
|
||||
"/api/v1/users/register",
|
||||
"/api/v1/users/login",
|
||||
"/api/v1/users/password/reset",
|
||||
"/api/v1/skills", // Skill列表公开
|
||||
"/api/v1/skills/{id}", // Skill详情公开
|
||||
"/api/v1/skills/search", // 搜索公开
|
||||
"/api/v1/payments/callback" // 支付回调无token
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**创建日期**:2026-03-16
|
||||
Reference in New Issue
Block a user