11 KiB
11 KiB
用户服务开发文档 - Part 2(Controller + 通用工具)
五、Controller
UserController.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
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
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
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
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
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认证拦截器)
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
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(注册拦截器)
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