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

11 KiB
Raw Blame History

用户服务开发文档 - Part 2Controller + 通用工具)

五、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.javaJWT认证拦截器

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