diff --git a/pom.xml b/pom.xml
index 877bba6..6fd1f5f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,9 @@
timeline-file-service
timeline-component-common
timeline-story-service
-
+ timeline-user-service
+ timeline-gateway-service
+
diff --git a/timeline-component-common/src/main/java/com/timeline/common/constants/CommonConstants.java b/timeline-component-common/src/main/java/com/timeline/common/constants/CommonConstants.java
index 7396bab..3b78ca3 100644
--- a/timeline-component-common/src/main/java/com/timeline/common/constants/CommonConstants.java
+++ b/timeline-component-common/src/main/java/com/timeline/common/constants/CommonConstants.java
@@ -14,4 +14,8 @@ public class CommonConstants {
public static final int DELETED = 1;
public static final int NOT_DELETED = 0;
public static final String LOW_RESOLUTION_PREFIX = "low_res_";
+
+ // 用户状态
+ public static final int USER_STATUS_NORMAL = 0;
+ public static final int USER_STATUS_DISABLED = 1;
}
diff --git a/timeline-component-common/src/main/java/com/timeline/common/utils/UserContextUtils.java b/timeline-component-common/src/main/java/com/timeline/common/utils/UserContextUtils.java
new file mode 100644
index 0000000..3e77542
--- /dev/null
+++ b/timeline-component-common/src/main/java/com/timeline/common/utils/UserContextUtils.java
@@ -0,0 +1,26 @@
+package com.timeline.common.utils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+public class UserContextUtils {
+
+ public static String getCurrentUserId() {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ return request.getHeader("X-User-Id");
+ }
+ return null;
+ }
+
+ public static String getCurrentUsername() {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ return request.getHeader("X-Username");
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/timeline-gateway-service/pom.xml b/timeline-gateway-service/pom.xml
new file mode 100644
index 0000000..0527bd6
--- /dev/null
+++ b/timeline-gateway-service/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ com.timeline
+ timeline
+ 0.0.1-SNAPSHOT
+
+
+ timeline-gateway-service
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+
+
+ com.timeline
+ timeline-component-common
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/timeline-gateway-service/src/main/java/com/timeline/gateway/TimelineGatewayApplication.java b/timeline-gateway-service/src/main/java/com/timeline/gateway/TimelineGatewayApplication.java
new file mode 100644
index 0000000..d5170bc
--- /dev/null
+++ b/timeline-gateway-service/src/main/java/com/timeline/gateway/TimelineGatewayApplication.java
@@ -0,0 +1,14 @@
+// TimelineGatewayApplication.java
+package com.timeline.gateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+@SpringBootApplication
+@EnableDiscoveryClient
+public class TimelineGatewayApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(TimelineGatewayApplication.class, args);
+ }
+}
diff --git a/timeline-gateway-service/src/main/java/com/timeline/gateway/config/GatewayConfig.java b/timeline-gateway-service/src/main/java/com/timeline/gateway/config/GatewayConfig.java
new file mode 100644
index 0000000..f674a1b
--- /dev/null
+++ b/timeline-gateway-service/src/main/java/com/timeline/gateway/config/GatewayConfig.java
@@ -0,0 +1,25 @@
+// GatewayConfig.java
+package com.timeline.gateway.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.reactive.CorsWebFilter;
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
+
+@Configuration
+public class GatewayConfig {
+
+ @Bean
+ public CorsWebFilter corsWebFilter() {
+ CorsConfiguration config = new CorsConfiguration();
+ config.addAllowedOrigin("*");
+ config.addAllowedMethod("*");
+ config.addAllowedHeader("*");
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+
+ return new CorsWebFilter(source);
+ }
+}
diff --git a/timeline-gateway-service/src/main/java/com/timeline/gateway/filter/AuthenticationFilter.java b/timeline-gateway-service/src/main/java/com/timeline/gateway/filter/AuthenticationFilter.java
new file mode 100644
index 0000000..65f0437
--- /dev/null
+++ b/timeline-gateway-service/src/main/java/com/timeline/gateway/filter/AuthenticationFilter.java
@@ -0,0 +1,103 @@
+package com.timeline.gateway.filter;
+import com.timeline.common.response.ResponseEntity;
+import com.timeline.common.response.ResponseEnum;
+import com.timeline.gateway.util.JwtUtils;
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+
+@Component
+@Slf4j
+public class AuthenticationFilter implements GlobalFilter, Ordered {
+
+ @Autowired
+ private JwtUtils jwtUtils;
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ ServerHttpRequest request = exchange.getRequest();
+ String path = request.getURI().getPath();
+
+ // 白名单路径不需要认证
+ if (isWhitelisted(path)) {
+ return chain.filter(exchange);
+ }
+
+ // 从请求头中获取token
+ String token = extractToken(request);
+
+ if (token == null || token.isEmpty()) {
+ return handleUnauthorized(exchange, ResponseEnum.UNAUTHORIZED.getMessage());
+ }
+
+ // 验证token
+ if (!jwtUtils.validateToken(token)) {
+ return handleUnauthorized(exchange, "无效的Token");
+ }
+
+ Claims claims = jwtUtils.getClaimsFromToken(token);
+ if (claims == null || jwtUtils.isTokenExpired(claims)) {
+ return handleUnauthorized(exchange, "Token已过期");
+ }
+
+ // 将用户信息添加到请求头中传递给下游服务
+ String userId = jwtUtils.getUserIdFromClaims(claims);
+ String username = jwtUtils.getUsernameFromClaims(claims);
+
+ ServerHttpRequest mutatedRequest = request.mutate()
+ .header("X-User-Id", userId)
+ .header("X-Username", username)
+ .build();
+
+ ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
+
+ return chain.filter(mutatedExchange);
+ }
+
+ @Override
+ public int getOrder() {
+ return -100; // 设置过滤器优先级,确保在其他过滤器之前执行
+ }
+
+ private boolean isWhitelisted(String path) {
+ // 白名单路径不需要认证
+ return path.startsWith("/auth/login") ||
+ path.startsWith("/auth/register") ||
+ path.startsWith("/ping") ||
+ path.startsWith("/actuator");
+ }
+
+ private String extractToken(ServerHttpRequest request) {
+ String bearerToken = request.getHeaders().getFirst("Authorization");
+ if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
+ return bearerToken.substring(7);
+ }
+ return null;
+ }
+
+ private Mono handleUnauthorized(ServerWebExchange exchange, String message) {
+ ServerHttpResponse response = exchange.getResponse();
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
+
+ ResponseEntity responseEntity = ResponseEntity.error(ResponseEnum.UNAUTHORIZED.getCode(), message);
+ String responseBody = com.alibaba.fastjson.JSON.toJSONString(responseEntity);
+
+ byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8);
+ DataBuffer buffer = response.bufferFactory().wrap(bytes);
+
+ return response.writeWith(Mono.just(buffer));
+ }
+}
diff --git a/timeline-gateway-service/src/main/java/com/timeline/gateway/utils/JwtUtils.java b/timeline-gateway-service/src/main/java/com/timeline/gateway/utils/JwtUtils.java
new file mode 100644
index 0000000..aa56910
--- /dev/null
+++ b/timeline-gateway-service/src/main/java/com/timeline/gateway/utils/JwtUtils.java
@@ -0,0 +1,56 @@
+// JwtUtils.java
+package com.timeline.gateway.util;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JwtUtils {
+
+ @Value("${jwt.secret:timelineSecretKey}")
+ private String secret;
+
+ public Claims getClaimsFromToken(String token) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(secret)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public boolean validateToken(String token) {
+ try {
+ Jwts.parserBuilder().setSigningKey(secret).build().parseClaimsJws(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public boolean isTokenExpired(Claims claims) {
+ if (claims == null) {
+ return true;
+ }
+ return claims.getExpiration().before(new java.util.Date());
+ }
+
+ public String getUserIdFromClaims(Claims claims) {
+ if (claims == null) {
+ return null;
+ }
+ return claims.get("userId", String.class);
+ }
+
+ public String getUsernameFromClaims(Claims claims) {
+ if (claims == null) {
+ return null;
+ }
+ return claims.getSubject();
+ }
+}
diff --git a/timeline-gateway-service/src/main/resources/application.properties b/timeline-gateway-service/src/main/resources/application.properties
new file mode 100644
index 0000000..e5ff041
--- /dev/null
+++ b/timeline-gateway-service/src/main/resources/application.properties
@@ -0,0 +1,27 @@
+# application.properties
+spring.application.name=timeline-gateway
+
+server.port=30000
+
+# ????
+spring.cloud.gateway.routes[0].id=story-service
+spring.cloud.gateway.routes[0].uri=lb://timeline.story
+spring.cloud.gateway.routes[0].predicates[0]=Path=/story/**
+spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
+
+spring.cloud.gateway.routes[1].id=file-service
+spring.cloud.gateway.routes[1].uri=lb://timeline.file
+spring.cloud.gateway.routes[1].predicates[0]=Path=/file/**
+spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
+
+# JWT??
+jwt.secret=timelineSecretKey
+jwt.expiration=86400
+
+# Actuator??
+management.endpoints.web.exposure.include=*
+management.endpoint.health.show-details=always
+
+# ????
+logging.level.org.springframework.cloud.gateway=DEBUG
+logging.level.com.timeline.gateway=DEBUG
diff --git a/timeline-user-service/pom.xml b/timeline-user-service/pom.xml
new file mode 100644
index 0000000..033cddb
--- /dev/null
+++ b/timeline-user-service/pom.xml
@@ -0,0 +1,113 @@
+
+
+ 4.0.0
+
+ com.timeline
+ timeline
+ 0.0.1-SNAPSHOT
+
+
+ timeline-user-service
+ timeline-user-service
+ User management service for timeline system
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+
+
+ com.timeline
+ timeline-component-common
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 3.0.4
+
+
+ com.mysql
+ mysql-connector-j
+ 8.2.0
+ runtime
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+
+
+
+
diff --git a/timeline-user-service/src/main/java/com/timeline/user/TimelineUserServiceApplication.java b/timeline-user-service/src/main/java/com/timeline/user/TimelineUserServiceApplication.java
new file mode 100644
index 0000000..5d3d154
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/TimelineUserServiceApplication.java
@@ -0,0 +1,14 @@
+package com.timeline.user;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication
+@ComponentScan({"com.timeline", "com.timeline.user"})
+public class TimelineUserServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TimelineUserServiceApplication.class, args);
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/config/JwtConfig.java b/timeline-user-service/src/main/java/com/timeline/user/config/JwtConfig.java
new file mode 100644
index 0000000..959108f
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/config/JwtConfig.java
@@ -0,0 +1,28 @@
+package com.timeline.user.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "jwt")
+public class JwtConfig {
+ private String secret = "timelineSecretKey";
+ private Long expiration = 86400L; // 24小时
+
+ // Getters and setters
+ public String getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public Long getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Long expiration) {
+ this.expiration = expiration;
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/config/SecurityConfig.java b/timeline-user-service/src/main/java/com/timeline/user/config/SecurityConfig.java
new file mode 100644
index 0000000..469fc29
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/config/SecurityConfig.java
@@ -0,0 +1,15 @@
+package com.timeline.user.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class SecurityConfig {
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/config/WebMvcConfig.java b/timeline-user-service/src/main/java/com/timeline/user/config/WebMvcConfig.java
new file mode 100644
index 0000000..6ee9461
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/config/WebMvcConfig.java
@@ -0,0 +1,21 @@
+package com.timeline.user.config;
+
+import com.timeline.user.interceptor.UserContextInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ @Autowired
+ private UserContextInterceptor userContextInterceptor;
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(userContextInterceptor)
+ .addPathPatterns("/api/**")
+ .excludePathPatterns("/api/auth/login", "/api/auth/register");
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/controller/AuthController.java b/timeline-user-service/src/main/java/com/timeline/user/controller/AuthController.java
new file mode 100644
index 0000000..06d5872
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/controller/AuthController.java
@@ -0,0 +1,34 @@
+package com.timeline.user.controller;
+
+import com.timeline.common.response.ResponseEntity;
+import com.timeline.user.dto.LoginRequest;
+import com.timeline.user.dto.LoginResponse;
+import com.timeline.user.dto.RegisterRequest;
+import com.timeline.user.entity.User;
+import com.timeline.user.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ @Autowired
+ private UserService userService;
+
+ @PostMapping("/login")
+ public ResponseEntity login(@RequestBody LoginRequest loginRequest) {
+ log.info("用户登录请求: {}", loginRequest.getUsername());
+ LoginResponse response = userService.login(loginRequest);
+ return ResponseEntity.success(response);
+ }
+
+ @PostMapping("/register")
+ public ResponseEntity register(@RequestBody RegisterRequest registerRequest) {
+ log.info("用户注册请求: {}", registerRequest.getUsername());
+ User user = userService.register(registerRequest);
+ return ResponseEntity.success(user);
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/controller/PermissionController.java b/timeline-user-service/src/main/java/com/timeline/user/controller/PermissionController.java
new file mode 100644
index 0000000..4fa11dd
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/controller/PermissionController.java
@@ -0,0 +1,25 @@
+package com.timeline.user.controller;
+
+import com.timeline.common.response.ResponseEntity;
+import com.timeline.user.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/permission")
+public class PermissionController {
+
+ @Autowired
+ private UserService userService;
+
+ @GetMapping("/check")
+ public ResponseEntity checkUserPermission(
+ @RequestParam String userId,
+ @RequestParam String requiredPermission) {
+ log.info("检查用户权限: userId={}, requiredPermission={}", userId, requiredPermission);
+ boolean hasPermission = userService.checkUserPermission(userId, requiredPermission);
+ return ResponseEntity.success(hasPermission);
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/controller/UserController.java b/timeline-user-service/src/main/java/com/timeline/user/controller/UserController.java
new file mode 100644
index 0000000..50f4e02
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/controller/UserController.java
@@ -0,0 +1,43 @@
+package com.timeline.user.controller;
+
+import com.timeline.common.response.ResponseEntity;
+import com.timeline.user.dto.RegisterRequest;
+import com.timeline.user.entity.User;
+import com.timeline.user.service.UserService;
+import com.timeline.user.utils.UserContext;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/user")
+public class UserController {
+
+ @Autowired
+ private UserService userService;
+
+ @GetMapping("/info")
+ public ResponseEntity getCurrentUserInfo() {
+ String userId = UserContext.getCurrentUserId();
+ log.info("获取当前用户信息: {}", userId);
+ User user = userService.getUserByUserId(userId);
+ return ResponseEntity.success(user);
+ }
+
+ @PutMapping("/info")
+ public ResponseEntity updateUserInfo(@RequestBody RegisterRequest updateRequest) {
+ String userId = UserContext.getCurrentUserId();
+ log.info("更新用户信息: {}", userId);
+ User user = userService.updateUserInfo(userId, updateRequest);
+ return ResponseEntity.success(user);
+ }
+
+ @DeleteMapping
+ public ResponseEntity deleteUser() {
+ String userId = UserContext.getCurrentUserId();
+ log.info("删除用户: {}", userId);
+ userService.deleteUser(userId);
+ return ResponseEntity.success("用户删除成功");
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/dao/UserMapper.java b/timeline-user-service/src/main/java/com/timeline/user/dao/UserMapper.java
new file mode 100644
index 0000000..5e5e284
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/dao/UserMapper.java
@@ -0,0 +1,14 @@
+package com.timeline.user.dao;
+
+import com.timeline.user.entity.User;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserMapper {
+ void insert(User user);
+ User selectById(Long id);
+ User selectByUserId(String userId);
+ User selectByUsername(String username);
+ void update(User user);
+ void deleteByUserId(String userId);
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/dto/LoginRequest.java b/timeline-user-service/src/main/java/com/timeline/user/dto/LoginRequest.java
new file mode 100644
index 0000000..725a20d
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/dto/LoginRequest.java
@@ -0,0 +1,9 @@
+package com.timeline.user.dto;
+
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+ private String username;
+ private String password;
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/dto/LoginResponse.java b/timeline-user-service/src/main/java/com/timeline/user/dto/LoginResponse.java
new file mode 100644
index 0000000..bf10838
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/dto/LoginResponse.java
@@ -0,0 +1,12 @@
+package com.timeline.user.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class LoginResponse {
+ private String token;
+ private String userId;
+ private String username;
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/dto/RegisterRequest.java b/timeline-user-service/src/main/java/com/timeline/user/dto/RegisterRequest.java
new file mode 100644
index 0000000..8cd0cb5
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/dto/RegisterRequest.java
@@ -0,0 +1,12 @@
+package com.timeline.user.dto;
+
+import lombok.Data;
+
+@Data
+public class RegisterRequest {
+ private String username;
+ private String nickname;
+ private String password;
+ private String email;
+ private String phone;
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/entity/User.java b/timeline-user-service/src/main/java/com/timeline/user/entity/User.java
new file mode 100644
index 0000000..8791885
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/entity/User.java
@@ -0,0 +1,22 @@
+package com.timeline.user.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+public class User {
+ private Long id;
+ private String userId;
+ private String username;
+ private String password;
+ private String nickname;
+ private String email;
+ private String phone;
+ private Integer status; // 0-正常,1-禁用
+ private Integer isDeleted; // 0-未删除,1-已删除
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime updateTime;
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/interceptor/UserContextInterceptor.java b/timeline-user-service/src/main/java/com/timeline/user/interceptor/UserContextInterceptor.java
new file mode 100644
index 0000000..d6b2cb3
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/interceptor/UserContextInterceptor.java
@@ -0,0 +1,42 @@
+package com.timeline.user.interceptor;
+
+import com.timeline.user.entity.User;
+import com.timeline.user.service.UserService;
+import com.timeline.user.utils.JwtUtils;
+import com.timeline.user.utils.UserContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class UserContextInterceptor implements HandlerInterceptor {
+
+ @Autowired
+ private JwtUtils jwtUtils;
+
+ @Autowired
+ private UserService userService;
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ String token = request.getHeader("Authorization");
+ if (token != null && token.startsWith("Bearer ")) {
+ token = token.substring(7);
+ if (jwtUtils.validateToken(token) && !jwtUtils.isTokenExpired(token)) {
+ String userId = jwtUtils.getUserIdFromToken(token);
+ User user = userService.getUserByUserId(userId);
+ if (user != null) {
+ UserContext.setUser(user);
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ UserContext.clear();
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/service/UserService.java b/timeline-user-service/src/main/java/com/timeline/user/service/UserService.java
new file mode 100644
index 0000000..14cc5bb
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/service/UserService.java
@@ -0,0 +1,15 @@
+package com.timeline.user.service;
+
+import com.timeline.user.entity.User;
+import com.timeline.user.dto.LoginRequest;
+import com.timeline.user.dto.RegisterRequest;
+import com.timeline.user.dto.LoginResponse;
+
+public interface UserService {
+ LoginResponse login(LoginRequest loginRequest);
+ User register(RegisterRequest registerRequest);
+ User getUserByUserId(String userId);
+ User updateUserInfo(String userId, RegisterRequest updateRequest);
+ void deleteUser(String userId);
+ boolean checkUserPermission(String userId, String requiredPermission);
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/service/impl/UserServiceImpl.java b/timeline-user-service/src/main/java/com/timeline/user/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..3d5192a
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/service/impl/UserServiceImpl.java
@@ -0,0 +1,142 @@
+package com.timeline.user.service.impl;
+
+import com.timeline.common.constants.CommonConstants;
+import com.timeline.common.exception.CustomException;
+import com.timeline.common.response.ResponseEnum;
+import com.timeline.common.utils.IdUtils;
+import com.timeline.user.dao.UserMapper;
+import com.timeline.user.entity.User;
+import com.timeline.user.dto.LoginRequest;
+import com.timeline.user.dto.RegisterRequest;
+import com.timeline.user.dto.LoginResponse;
+import com.timeline.user.service.UserService;
+import com.timeline.user.utils.JwtUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Service
+public class UserServiceImpl implements UserService {
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Autowired
+ private PasswordEncoder passwordEncoder;
+
+ @Autowired
+ private JwtUtils jwtUtils;
+
+ @Override
+ public LoginResponse login(LoginRequest loginRequest) {
+ try {
+ User user = userMapper.selectByUsername(loginRequest.getUsername());
+ if (user == null) {
+ throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
+ }
+
+ if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
+ throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
+ }
+
+ if (user.getStatus() == 1) {
+ throw new CustomException(ResponseEnum.FORBIDDEN, "用户已被禁用");
+ }
+
+ String token = jwtUtils.generateToken(user.getUserId(), user.getUsername());
+ return new LoginResponse(token, user.getUserId(), user.getUsername());
+ } catch (CustomException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("用户登录失败", e);
+ throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "登录失败");
+ }
+ }
+
+ @Override
+ public User register(RegisterRequest registerRequest) {
+ try {
+ // 检查用户名是否已存在
+ User existingUser = userMapper.selectByUsername(registerRequest.getUsername());
+ if (existingUser != null) {
+ throw new CustomException(ResponseEnum.BAD_REQUEST, "用户名已存在");
+ }
+
+ User user = new User();
+ user.setUserId(IdUtils.randomUuidUpper());
+ user.setUsername(registerRequest.getUsername());
+ user.setNickname(registerRequest.getNickname());
+ user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
+ user.setEmail(registerRequest.getEmail());
+ user.setPhone(registerRequest.getPhone());
+ user.setStatus(CommonConstants.USER_STATUS_NORMAL); // 正常状态
+ user.setIsDeleted(CommonConstants.NOT_DELETED);
+ user.setCreateTime(LocalDateTime.now());
+ user.setUpdateTime(LocalDateTime.now());
+
+ userMapper.insert(user);
+ return user;
+ } catch (CustomException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("用户注册失败", e);
+ throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "注册失败");
+ }
+ }
+
+ @Override
+ public User getUserByUserId(String userId) {
+ return userMapper.selectByUserId(userId);
+ }
+
+ @Override
+ public User updateUserInfo(String userId, RegisterRequest updateRequest) {
+ try {
+ User user = userMapper.selectByUserId(userId);
+ if (user == null) {
+ throw new CustomException(ResponseEnum.NOT_FOUND, "用户不存在");
+ }
+
+ user.setEmail(updateRequest.getEmail());
+ user.setPhone(updateRequest.getPhone());
+ user.setUpdateTime(LocalDateTime.now());
+
+ userMapper.update(user);
+ return user;
+ } catch (CustomException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("更新用户信息失败", e);
+ throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "更新用户信息失败");
+ }
+ }
+
+ @Override
+ public void deleteUser(String userId) {
+ try {
+ User user = userMapper.selectByUserId(userId);
+ if (user == null) {
+ throw new CustomException(ResponseEnum.NOT_FOUND, "用户不存在");
+ }
+
+ userMapper.deleteByUserId(userId);
+ } catch (CustomException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("删除用户失败", e);
+ throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "删除用户失败");
+ }
+ }
+
+ @Override
+ public boolean checkUserPermission(String userId, String requiredPermission) {
+ // 这里可以实现更复杂的权限检查逻辑
+ // 简单示例:检查用户是否存在
+ User user = userMapper.selectByUserId(userId);
+ return user != null && user.getStatus() == 0; // 用户存在且未被禁用
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/utils/JwtUtils.java b/timeline-user-service/src/main/java/com/timeline/user/utils/JwtUtils.java
new file mode 100644
index 0000000..f0d4402
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/utils/JwtUtils.java
@@ -0,0 +1,63 @@
+package com.timeline.user.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class JwtUtils {
+
+ @Value("${jwt.secret:timelineSecretKey}")
+ private String secret;
+
+ @Value("${jwt.expiration:86400}")
+ private Long expiration;
+
+ public String generateToken(String userId, String username) {
+ Map claims = new HashMap<>();
+ claims.put("userId", userId);
+ claims.put("username", username);
+ return Jwts.builder()
+ .setClaims(claims)
+ .setSubject(username)
+ .setIssuedAt(new Date())
+ .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
+ .signWith(SignatureAlgorithm.HS512, secret)
+ .compact();
+ }
+
+ public Claims getClaimsFromToken(String token) {
+ return Jwts.parser()
+ .setSigningKey(secret)
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ public String getUserIdFromToken(String token) {
+ return getClaimsFromToken(token).get("userId", String.class);
+ }
+
+ public String getUsernameFromToken(String token) {
+ return getClaimsFromToken(token).getSubject();
+ }
+
+ public boolean validateToken(String token) {
+ try {
+ Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public boolean isTokenExpired(String token) {
+ Date expiration = getClaimsFromToken(token).getExpiration();
+ return expiration.before(new Date());
+ }
+}
diff --git a/timeline-user-service/src/main/java/com/timeline/user/utils/UserContext.java b/timeline-user-service/src/main/java/com/timeline/user/utils/UserContext.java
new file mode 100644
index 0000000..09c83de
--- /dev/null
+++ b/timeline-user-service/src/main/java/com/timeline/user/utils/UserContext.java
@@ -0,0 +1,29 @@
+package com.timeline.user.utils;
+
+import com.timeline.user.entity.User;
+
+public class UserContext {
+ private static final ThreadLocal userHolder = new ThreadLocal<>();
+
+ public static void setUser(User user) {
+ userHolder.set(user);
+ }
+
+ public static User getCurrentUser() {
+ return userHolder.get();
+ }
+
+ public static String getCurrentUserId() {
+ User user = getCurrentUser();
+ return user != null ? user.getUserId() : null;
+ }
+
+ public static String getCurrentUsername() {
+ User user = getCurrentUser();
+ return user != null ? user.getUsername() : null;
+ }
+
+ public static void clear() {
+ userHolder.remove();
+ }
+}
diff --git a/timeline-user-service/src/main/resources/application.properties b/timeline-user-service/src/main/resources/application.properties
new file mode 100644
index 0000000..eaf7c18
--- /dev/null
+++ b/timeline-user-service/src/main/resources/application.properties
@@ -0,0 +1,20 @@
+spring.application.name=timeline.user
+server.port=30003
+
+# ?????
+spring.datasource.url=jdbc:mysql://8.137.148.196:33306/timeline?serverTimezone=UTC&allowPublicKeyRetrieval=true
+spring.datasource.username=root
+spring.datasource.password=your_password
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+# MyBatis ??
+mybatis.mapper-locations=classpath:mapper/*.xml
+mybatis.type-aliases-package=com.timeline.user.entity
+mybatis.configuration.mapUnderscoreToCamelCase=true
+
+# JWT ??
+jwt.secret=timelineSecretKey
+jwt.expiration=86400
+
+# ????
+logging.level.com.timeline.user=DEBUG
diff --git a/timeline-user-service/src/main/resources/com/timeline/user/dao/UserMapper.xml b/timeline-user-service/src/main/resources/com/timeline/user/dao/UserMapper.xml
new file mode 100644
index 0000000..1f501aa
--- /dev/null
+++ b/timeline-user-service/src/main/resources/com/timeline/user/dao/UserMapper.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ INSERT INTO user (user_id, username, password, nickname, email, phone, status, is_deleted, create_time, update_time)
+ VALUES (#{userId}, #{username}, #{password}, #{nickname}, #{email}, #{phone}, #{status}, #{isDeleted}, #{createTime}, #{updateTime})
+
+
+
+
+
+
+
+
+
+ UPDATE user
+ SET username = #{username},
+ email = #{email},
+ phone = #{phone},
+ status = #{status},
+ update_time = #{updateTime}
+ WHERE user_id = #{userId} AND is_deleted = 0
+
+
+
+ UPDATE user SET is_deleted = 1, update_time = NOW() WHERE user_id = #{userId}
+
+