# 用户服务开发文档 - 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 sendSmsCode(@RequestParam String phone) { userService.sendSmsCode(phone); return Result.ok(); } /** 用户注册 */ @PostMapping("/register") public Result register(@Valid @RequestBody UserRegisterDTO dto) { return Result.ok(userService.register(dto)); } /** 用户登录 */ @PostMapping("/login") public Result login(@Valid @RequestBody UserLoginDTO dto) { return Result.ok(userService.login(dto)); } /** 退出登录 */ @PostMapping("/logout") public Result logout(@RequestHeader("Authorization") String authorization) { String token = authorization.replace("Bearer ", ""); userService.logout(token); return Result.ok(); } /** 获取当前用户信息 */ @GetMapping("/profile") public Result getProfile() { return Result.ok(userService.getCurrentUser(UserContext.getUserId())); } /** 更新个人信息 */ @PutMapping("/profile") public Result updateProfile(@RequestBody UserUpdateDTO dto) { return Result.ok(userService.updateProfile(UserContext.getUserId(), dto)); } /** 修改密码 */ @PutMapping("/password") public Result changePassword( @RequestParam String oldPassword, @RequestParam String newPassword) { userService.changePassword(UserContext.getUserId(), oldPassword, newPassword); return Result.ok(); } /** 忘记密码 - 重置 */ @PostMapping("/password/reset") public Result 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 { private Integer code; private String message; private T data; private Long timestamp = System.currentTimeMillis(); public static Result ok(T data) { Result r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static Result ok() { return ok(null); } public static Result error(int code, String message) { Result 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 handleBusiness(BusinessException e) { log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(BindException.class) public Result handleValidation(BindException e) { String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); return Result.error(400, msg); } @ExceptionHandler(Exception.class) public Result 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 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