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

375 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 用户服务开发文档 - Part 2Controller + 通用工具)
## 五、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.javaJWT认证拦截器
```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