375 lines
11 KiB
Markdown
375 lines
11 KiB
Markdown
# 用户服务开发文档 - 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
|