commit 4fe7ba5d0d781c47b3a2c293f223810036568981 Author: panxuejie <15855548138@163.com> Date: Tue Dec 30 11:14:25 2025 +0800 Initial commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..69fcbcd --- /dev/null +++ b/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + com.tacit + tacit-parent + 1.0.0-SNAPSHOT + pom + + Tacit Parent + Tacit App Microservice Architecture Parent Project + + + 21 + 3.2.0 + 2023.0.0 + 2023.0.1.0 + 3.5.5 + 1.18.30 + 0.11.5 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + + tacit-gateway + tacit-admin + tacit-app-api + tacit-common + + + + + central + Maven Central Repository + https://repo1.maven.org/maven2/ + default + + true + + + false + + + + + + + central + Maven Central Repository + https://repo1.maven.org/maven2/ + default + + true + + + false + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + UTF-8 + + + + + + + \ No newline at end of file diff --git a/tacit-admin/pom.xml b/tacit-admin/pom.xml new file mode 100644 index 0000000..86dee2b --- /dev/null +++ b/tacit-admin/pom.xml @@ -0,0 +1,101 @@ + + + + tacit-parent + com.tacit + 1.0.0-SNAPSHOT + + 4.0.0 + + tacit-admin + Tacit Admin + Admin Service for Tacit Microservices + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.oceanbase + oceanbase-client + 2.4.0 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + com.tacit + tacit-common + ${project.parent.version} + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + false + + + + + \ No newline at end of file diff --git a/tacit-admin/src/main/java/com/tacit/admin/AdminApplication.java b/tacit-admin/src/main/java/com/tacit/admin/AdminApplication.java new file mode 100644 index 0000000..cf6e603 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/AdminApplication.java @@ -0,0 +1,17 @@ +package com.tacit.admin; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.tacit.admin.mapper") +public class AdminApplication { + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java b/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java new file mode 100644 index 0000000..55841d0 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java @@ -0,0 +1,114 @@ +package com.tacit.admin.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // 禁用CSRF保护 + .csrf(csrf -> csrf.disable()) + // 允许跨域请求 + .cors(cors -> cors.disable()) + // 设置会话管理为无状态 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 添加JWT认证过滤器 + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) + // 设置请求授权规则 + .authorizeHttpRequests(auth -> auth + // 允许访问的路径 + .requestMatchers("/test/**", "/user/login", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + // 其他请求需要认证 + .anyRequest().authenticated() + ); + + return http.build(); + } + + @Bean + public OncePerRequestFilter jwtAuthenticationFilter() { + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 从请求头中获取用户信息(由网关添加) + String userId = request.getHeader("X-User-Id"); + String username = request.getHeader("X-Username"); + String role = request.getHeader("X-Role"); + + // 如果有用户信息,则创建Authentication对象并设置到SecurityContext + if (userId != null && username != null && role != null) { + // 创建用户对象 + org.springframework.security.core.userdetails.User user = new User( + username, + "", + Collections.singletonList(() -> "ROLE_" + role.toUpperCase()) + ); + + // 创建认证对象 + org.springframework.security.authentication.UsernamePasswordAuthenticationToken authentication = + new org.springframework.security.authentication.UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + + // 设置认证信息到SecurityContext + org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authentication); + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + }; + } + + @Bean + public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder); + return new ProviderManager(Collections.singletonList(provider)); + } + + @Bean + public UserDetailsService userDetailsService() { + // 这里可以根据实际情况实现UserDetailsService,从数据库中获取用户信息 + return username -> { + // 由于我们使用网关进行认证,这里可以返回一个空实现 + // 实际项目中应该根据用户名从数据库中获取用户信息 + throw new UsernameNotFoundException("User not found: " + username); + }; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/config/SwaggerConfig.java b/tacit-admin/src/main/java/com/tacit/admin/config/SwaggerConfig.java new file mode 100644 index 0000000..c233c2a --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.tacit.admin.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger配置类 + * 使用SpringDoc OpenAPI配置API文档 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI adminOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Tacit Admin API") + .description("管理台服务API文档") + .version("1.0.0")); + } +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/controller/TestController.java b/tacit-admin/src/main/java/com/tacit/admin/controller/TestController.java new file mode 100644 index 0000000..d885e0d --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/controller/TestController.java @@ -0,0 +1,32 @@ +package com.tacit.admin.controller; + +import com.tacit.common.entity.ResponseResult; +import com.tacit.common.feign.AppApiFeignClient; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +@Tag(name = "测试接口", description = "用于测试的接口") +public class TestController { + + @Autowired + private AppApiFeignClient appApiFeignClient; + + @Operation(summary = "测试接口", description = "这是一个测试接口") + @GetMapping("/hello") + public ResponseResult hello() { + return ResponseResult.success("Hello from admin service!"); + } + + @Operation(summary = "测试Feign调用", description = "测试通过Feign调用APP接口服务") + @GetMapping("/feign/{userId}") + public ResponseResult testFeign(@PathVariable Long userId) { + return appApiFeignClient.getUserInfo(userId); + } +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/controller/UserController.java b/tacit-admin/src/main/java/com/tacit/admin/controller/UserController.java new file mode 100644 index 0000000..9752e5b --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/controller/UserController.java @@ -0,0 +1,70 @@ +package com.tacit.admin.controller; + +import com.tacit.admin.entity.User; +import com.tacit.admin.entity.dto.LoginRequest; +import com.tacit.admin.entity.dto.LoginResponse; +import com.tacit.admin.service.UserService; +import com.tacit.common.entity.ResponseResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/user") +@Tag(name = "用户管理", description = "用户相关接口") +public class UserController { + + @Autowired + private UserService userService; + + @Operation(summary = "用户登录", description = "用户登录获取JWT令牌") + @PostMapping("/login") + public ResponseResult login(@RequestBody LoginRequest loginRequest) { + LoginResponse loginResponse = userService.login(loginRequest); + return ResponseResult.success(loginResponse); + } + + @Operation(summary = "获取所有用户", description = "获取系统中所有用户列表") + @GetMapping("/list") + @PreAuthorize("hasRole('ADMIN')") + public ResponseResult> getAllUsers() { + List users = userService.getAllUsers(); + return ResponseResult.success(users); + } + + @Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户详情") + @GetMapping("/info/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseResult getUserById(@PathVariable Long id) { + User user = userService.getUserById(id); + return ResponseResult.success(user); + } + + @Operation(summary = "创建用户", description = "创建新用户") + @PostMapping("/create") + @PreAuthorize("hasRole('ADMIN')") + public ResponseResult createUser(@RequestBody User user) { + boolean result = userService.createUser(user); + return ResponseResult.success(result); + } + + @Operation(summary = "更新用户", description = "更新用户信息") + @PutMapping("/update") + @PreAuthorize("hasRole('ADMIN')") + public ResponseResult updateUser(@RequestBody User user) { + boolean result = userService.updateUser(user); + return ResponseResult.success(result); + } + + @Operation(summary = "删除用户", description = "根据用户ID删除用户") + @DeleteMapping("/delete/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseResult deleteUser(@PathVariable Long id) { + boolean result = userService.deleteUser(id); + return ResponseResult.success(result); + } +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/entity/User.java b/tacit-admin/src/main/java/com/tacit/admin/entity/User.java new file mode 100644 index 0000000..d210d9c --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/entity/User.java @@ -0,0 +1,28 @@ +package com.tacit.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@TableName("t_user") +public class User implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + private String username; + private String password; + private String nickname; + private String email; + private String phone; + private Integer status; + private String role; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Integer delFlag; +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginRequest.java b/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginRequest.java new file mode 100644 index 0000000..6fbb576 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginRequest.java @@ -0,0 +1,23 @@ +package com.tacit.admin.entity.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 登录请求DTO + */ +@Data +public class LoginRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; +} \ No newline at end of file diff --git a/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginResponse.java b/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginResponse.java new file mode 100644 index 0000000..4e15692 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/entity/dto/LoginResponse.java @@ -0,0 +1,24 @@ +package com.tacit.admin.entity.dto; + +import com.tacit.admin.entity.User; +import lombok.Data; + +import java.io.Serializable; + +/** + * 登录响应DTO + */ +@Data +public class LoginResponse implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * JWT令牌 + */ + private String token; + + /** + * 用户信息 + */ + private User user; +} \ No newline at end of file diff --git a/tacit-admin/src/main/java/com/tacit/admin/mapper/UserMapper.java b/tacit-admin/src/main/java/com/tacit/admin/mapper/UserMapper.java new file mode 100644 index 0000000..9598556 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.tacit.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.tacit.admin.entity.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/service/UserService.java b/tacit-admin/src/main/java/com/tacit/admin/service/UserService.java new file mode 100644 index 0000000..81c1667 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/service/UserService.java @@ -0,0 +1,58 @@ +package com.tacit.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.tacit.admin.entity.User; +import com.tacit.admin.entity.dto.LoginRequest; +import com.tacit.admin.entity.dto.LoginResponse; + +import java.util.List; + +public interface UserService extends IService { + /** + * 根据用户名查询用户 + * @param username 用户名 + * @return 用户信息 + */ + User getUserByUsername(String username); + + /** + * 根据用户ID查询用户 + * @param id 用户ID + * @return 用户信息 + */ + User getUserById(Long id); + + /** + * 获取所有用户列表 + * @return 用户列表 + */ + List getAllUsers(); + + /** + * 创建用户 + * @param user 用户信息 + * @return 创建结果 + */ + boolean createUser(User user); + + /** + * 更新用户信息 + * @param user 用户信息 + * @return 更新结果 + */ + boolean updateUser(User user); + + /** + * 删除用户 + * @param id 用户ID + * @return 删除结果 + */ + boolean deleteUser(Long id); + + /** + * 用户登录 + * @param loginRequest 登录请求参数 + * @return 登录响应结果 + */ + LoginResponse login(LoginRequest loginRequest); +} diff --git a/tacit-admin/src/main/java/com/tacit/admin/service/impl/UserServiceImpl.java b/tacit-admin/src/main/java/com/tacit/admin/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..efa24f4 --- /dev/null +++ b/tacit-admin/src/main/java/com/tacit/admin/service/impl/UserServiceImpl.java @@ -0,0 +1,98 @@ +package com.tacit.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.tacit.admin.entity.User; +import com.tacit.admin.entity.dto.LoginRequest; +import com.tacit.admin.entity.dto.LoginResponse; +import com.tacit.admin.mapper.UserMapper; +import com.tacit.admin.service.UserService; +import com.tacit.common.utils.JwtUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public User getUserByUsername(String username) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", username) + .eq("del_flag", 0); + return userMapper.selectOne(queryWrapper); + } + + @Override + public User getUserById(Long id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", id) + .eq("del_flag", 0); + return userMapper.selectOne(queryWrapper); + } + + @Override + public List getAllUsers() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("del_flag", 0) + .orderByDesc("create_time"); + return userMapper.selectList(queryWrapper); + } + + @Override + public boolean createUser(User user) { + user.setDelFlag(0); + return save(user); + } + + @Override + public boolean updateUser(User user) { + return updateById(user); + } + + @Override + public boolean deleteUser(Long id) { + User user = new User(); + user.setId(id); + user.setDelFlag(1); + return updateById(user); + } + + @Override + public LoginResponse login(LoginRequest loginRequest) { + // 根据用户名查询用户 + User user = getUserByUsername(loginRequest.getUsername()); + if (user == null) { + throw new RuntimeException("用户名或密码错误"); + } + + // 验证密码 + if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { + throw new RuntimeException("用户名或密码错误"); + } + + // 生成JWT令牌 + Map claims = new HashMap<>(); + claims.put("userId", user.getId()); + claims.put("username", user.getUsername()); + claims.put("role", user.getRole()); + String token = JwtUtils.generateToken(claims); + + // 构建登录响应 + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setToken(token); + loginResponse.setUser(user); + + return loginResponse; + } +} diff --git a/tacit-admin/src/main/resources/application-dev.yml b/tacit-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..e03d6e4 --- /dev/null +++ b/tacit-admin/src/main/resources/application-dev.yml @@ -0,0 +1,30 @@ +server: + port: 8081 + +spring: + datasource: + driver-class-name: com.oceanbase.jdbc.Driver + url: jdbc:oceanbase://localhost:2881/tacit?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: password + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:mapper/**/*.xml + type-aliases-package: com.tacit.admin.entity + +# Swagger Configuration +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + +# Logging Configuration +logging: + level: + com.tacit.admin: debug diff --git a/tacit-admin/src/main/resources/bootstrap.yml b/tacit-admin/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..4e12c8e --- /dev/null +++ b/tacit-admin/src/main/resources/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-admin + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-admin/target/classes/application-dev.yml b/tacit-admin/target/classes/application-dev.yml new file mode 100644 index 0000000..e03d6e4 --- /dev/null +++ b/tacit-admin/target/classes/application-dev.yml @@ -0,0 +1,30 @@ +server: + port: 8081 + +spring: + datasource: + driver-class-name: com.oceanbase.jdbc.Driver + url: jdbc:oceanbase://localhost:2881/tacit?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: password + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:mapper/**/*.xml + type-aliases-package: com.tacit.admin.entity + +# Swagger Configuration +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + +# Logging Configuration +logging: + level: + com.tacit.admin: debug diff --git a/tacit-admin/target/classes/bootstrap.yml b/tacit-admin/target/classes/bootstrap.yml new file mode 100644 index 0000000..4e12c8e --- /dev/null +++ b/tacit-admin/target/classes/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-admin + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-admin/target/classes/com/tacit/admin/AdminApplication.class b/tacit-admin/target/classes/com/tacit/admin/AdminApplication.class new file mode 100644 index 0000000..d73ed80 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/AdminApplication.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig$1.class b/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig$1.class new file mode 100644 index 0000000..e49c0fb Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig$1.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig.class b/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig.class new file mode 100644 index 0000000..63fd27d Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/config/SecurityConfig.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/config/SwaggerConfig.class b/tacit-admin/target/classes/com/tacit/admin/config/SwaggerConfig.class new file mode 100644 index 0000000..534bd78 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/config/SwaggerConfig.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/controller/TestController.class b/tacit-admin/target/classes/com/tacit/admin/controller/TestController.class new file mode 100644 index 0000000..61da2cd Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/controller/TestController.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/controller/UserController.class b/tacit-admin/target/classes/com/tacit/admin/controller/UserController.class new file mode 100644 index 0000000..027bd73 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/controller/UserController.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/entity/User.class b/tacit-admin/target/classes/com/tacit/admin/entity/User.class new file mode 100644 index 0000000..a3b98df Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/entity/User.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginRequest.class b/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginRequest.class new file mode 100644 index 0000000..1916b8a Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginRequest.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginResponse.class b/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginResponse.class new file mode 100644 index 0000000..6f94245 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/entity/dto/LoginResponse.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/mapper/UserMapper.class b/tacit-admin/target/classes/com/tacit/admin/mapper/UserMapper.class new file mode 100644 index 0000000..ae164c4 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/mapper/UserMapper.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/service/UserService.class b/tacit-admin/target/classes/com/tacit/admin/service/UserService.class new file mode 100644 index 0000000..fdc0f6d Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/service/UserService.class differ diff --git a/tacit-admin/target/classes/com/tacit/admin/service/impl/UserServiceImpl.class b/tacit-admin/target/classes/com/tacit/admin/service/impl/UserServiceImpl.class new file mode 100644 index 0000000..0678d41 Binary files /dev/null and b/tacit-admin/target/classes/com/tacit/admin/service/impl/UserServiceImpl.class differ diff --git a/tacit-app-api/pom.xml b/tacit-app-api/pom.xml new file mode 100644 index 0000000..3f4b5b2 --- /dev/null +++ b/tacit-app-api/pom.xml @@ -0,0 +1,101 @@ + + + + tacit-parent + com.tacit + 1.0.0-SNAPSHOT + + 4.0.0 + + tacit-app-api + Tacit App API + App API Service for Tacit Microservices + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.oceanbase + oceanbase-client + 2.4.0 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + com.tacit + tacit-common + ${project.parent.version} + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + false + + + + + \ No newline at end of file diff --git a/tacit-app-api/src/main/java/com/tacit/app/AppApiApplication.java b/tacit-app-api/src/main/java/com/tacit/app/AppApiApplication.java new file mode 100644 index 0000000..866915e --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/AppApiApplication.java @@ -0,0 +1,17 @@ +package com.tacit.app; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@MapperScan("com.tacit.app.mapper") +public class AppApiApplication { + public static void main(String[] args) { + SpringApplication.run(AppApiApplication.class, args); + } +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/config/SwaggerConfig.java b/tacit-app-api/src/main/java/com/tacit/app/config/SwaggerConfig.java new file mode 100644 index 0000000..4b4bbfc --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.tacit.app.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger配置类 + * 使用SpringDoc OpenAPI配置API文档 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI appApiOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Tacit App API") + .description("App接口服务API文档") + .version("1.0.0")); + } +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/controller/AuthController.java b/tacit-app-api/src/main/java/com/tacit/app/controller/AuthController.java new file mode 100644 index 0000000..7a24f29 --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/controller/AuthController.java @@ -0,0 +1,49 @@ +package com.tacit.app.controller; + +import com.tacit.app.entity.User; +import com.tacit.app.entity.dto.LoginRequest; +import com.tacit.app.entity.dto.LoginResponse; +import com.tacit.app.service.UserService; +import com.tacit.common.entity.ResponseResult; +import com.tacit.common.feign.AdminFeignClient; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +@Tag(name = "认证管理", description = "用户认证相关接口") +public class AuthController { + + @Autowired + private UserService userService; + + @Autowired + private AdminFeignClient adminFeignClient; + + @Operation(summary = "用户登录", description = "用户登录接口") + @PostMapping("/login") + public ResponseResult login(@RequestBody LoginRequest loginRequest) { + LoginResponse loginResponse = userService.login(loginRequest); + return ResponseResult.success(loginResponse); + } + + @Operation(summary = "用户注册", description = "用户注册接口") + @PostMapping("/register") + public ResponseResult register(@RequestBody User user) { + boolean result = userService.register(user); + return ResponseResult.success(result); + } + + @Operation(summary = "测试Feign调用", description = "测试通过Feign调用管理台服务") + @GetMapping("/test-feign/{userId}") + public ResponseResult testFeign(@PathVariable Long userId) { + return adminFeignClient.getUserById(userId); + } +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/controller/UserController.java b/tacit-app-api/src/main/java/com/tacit/app/controller/UserController.java new file mode 100644 index 0000000..ece8936 --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/controller/UserController.java @@ -0,0 +1,35 @@ +package com.tacit.app.controller; + +import com.tacit.app.entity.User; +import com.tacit.app.service.UserService; +import com.tacit.common.entity.ResponseResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/user") +@Tag(name = "用户管理", description = "用户相关接口") +public class UserController { + + @Autowired + private UserService userService; + + @Operation(summary = "获取用户信息", description = "根据用户ID获取用户信息") + @GetMapping("/info/{userId}") + @PreAuthorize("hasRole('USER')") + public ResponseResult getUserInfo(@PathVariable Long userId) { + User user = userService.getUserInfo(userId); + return ResponseResult.success(user); + } + + @Operation(summary = "更新用户信息", description = "更新用户个人信息") + @PutMapping("/update") + @PreAuthorize("hasRole('USER')") + public ResponseResult updateUserInfo(@RequestBody User user) { + boolean result = userService.updateUserInfo(user); + return ResponseResult.success(result); + } +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/entity/User.java b/tacit-app-api/src/main/java/com/tacit/app/entity/User.java new file mode 100644 index 0000000..a470433 --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/entity/User.java @@ -0,0 +1,28 @@ +package com.tacit.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@TableName("t_user") +public class User implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + private String username; + private String password; + private String nickname; + private String email; + private String phone; + private Integer status; + private String role; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Integer delFlag; +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginRequest.java b/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginRequest.java new file mode 100644 index 0000000..b4d725c --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package com.tacit.app.entity.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String username; + private String password; +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginResponse.java b/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginResponse.java new file mode 100644 index 0000000..48f6052 --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/entity/dto/LoginResponse.java @@ -0,0 +1,11 @@ +package com.tacit.app.entity.dto; + +import com.tacit.app.entity.User; +import lombok.Data; + +@Data +public class LoginResponse { + private User user; + private String token; + private Long expireTime; +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/mapper/UserMapper.java b/tacit-app-api/src/main/java/com/tacit/app/mapper/UserMapper.java new file mode 100644 index 0000000..5424eec --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.tacit.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.tacit.app.entity.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/service/UserService.java b/tacit-app-api/src/main/java/com/tacit/app/service/UserService.java new file mode 100644 index 0000000..348f9a6 --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/service/UserService.java @@ -0,0 +1,36 @@ +package com.tacit.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.tacit.app.entity.User; +import com.tacit.app.entity.dto.LoginRequest; +import com.tacit.app.entity.dto.LoginResponse; + +public interface UserService extends IService { + /** + * 用户登录 + * @param loginRequest 登录请求参数 + * @return 登录响应结果 + */ + LoginResponse login(LoginRequest loginRequest); + + /** + * 用户注册 + * @param user 用户信息 + * @return 注册结果 + */ + boolean register(User user); + + /** + * 根据用户ID获取用户信息 + * @param userId 用户ID + * @return 用户信息 + */ + User getUserInfo(Long userId); + + /** + * 更新用户信息 + * @param user 用户信息 + * @return 更新结果 + */ + boolean updateUserInfo(User user); +} diff --git a/tacit-app-api/src/main/java/com/tacit/app/service/impl/UserServiceImpl.java b/tacit-app-api/src/main/java/com/tacit/app/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..f3b7e9d --- /dev/null +++ b/tacit-app-api/src/main/java/com/tacit/app/service/impl/UserServiceImpl.java @@ -0,0 +1,115 @@ +package com.tacit.app.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.tacit.app.entity.User; +import com.tacit.app.entity.dto.LoginRequest; +import com.tacit.app.entity.dto.LoginResponse; +import com.tacit.app.mapper.UserMapper; +import com.tacit.app.service.UserService; +import com.tacit.common.constant.CommonConstant; +import com.tacit.common.exception.BusinessException; +import com.tacit.common.utils.JwtUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public LoginResponse login(LoginRequest loginRequest) { + // 根据用户名查询用户 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", loginRequest.getUsername()) + .eq("del_flag", 0); + User user = userMapper.selectOne(queryWrapper); + + // 检查用户是否存在 + if (user == null) { + throw new BusinessException("用户名或密码错误"); + } + + // 检查用户状态 + if (user.getStatus() == 0) { + throw new BusinessException("用户已禁用"); + } + + // 验证密码 + if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { + throw new BusinessException("用户名或密码错误"); + } + + // 生成JWT令牌 + Map claims = new HashMap<>(); + claims.put("userId", user.getId()); + claims.put("username", user.getUsername()); + claims.put("role", user.getRole()); + String token = JwtUtils.generateToken(claims); + + // 构造登录响应 + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setUser(user); + loginResponse.setToken(token); + loginResponse.setExpireTime(System.currentTimeMillis() + CommonConstant.JWT_EXPIRE_TIME); + + return loginResponse; + } + + @Override + public boolean register(User user) { + // 检查用户名是否已存在 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", user.getUsername()); + User existingUser = userMapper.selectOne(queryWrapper); + if (existingUser != null) { + throw new BusinessException("用户名已存在"); + } + + // 加密密码 + user.setPassword(passwordEncoder.encode(user.getPassword())); + // 设置默认角色 + user.setRole(CommonConstant.ROLE_USER); + // 设置默认状态 + user.setStatus(1); + // 设置删除标记 + user.setDelFlag(0); + + // 保存用户 + return save(user); + } + + @Override + public User getUserInfo(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", userId) + .eq("del_flag", 0); + return userMapper.selectOne(queryWrapper); + } + + @Override + public boolean updateUserInfo(User user) { + // 只更新允许修改的字段 + User existingUser = getUserInfo(user.getId()); + if (existingUser == null) { + throw new BusinessException("用户不存在"); + } + + // 更新用户信息 + existingUser.setNickname(user.getNickname()); + existingUser.setEmail(user.getEmail()); + existingUser.setPhone(user.getPhone()); + + return updateById(existingUser); + } +} diff --git a/tacit-app-api/src/main/resources/application-dev.yml b/tacit-app-api/src/main/resources/application-dev.yml new file mode 100644 index 0000000..543bc32 --- /dev/null +++ b/tacit-app-api/src/main/resources/application-dev.yml @@ -0,0 +1,30 @@ +server: + port: 8082 + +spring: + datasource: + driver-class-name: com.oceanbase.jdbc.Driver + url: jdbc:oceanbase://localhost:2881/tacit?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: password + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:mapper/**/*.xml + type-aliases-package: com.tacit.app.entity + +# Swagger Configuration +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + +# Logging Configuration +logging: + level: + com.tacit.app: debug diff --git a/tacit-app-api/src/main/resources/bootstrap.yml b/tacit-app-api/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..ad0ac67 --- /dev/null +++ b/tacit-app-api/src/main/resources/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-app-api + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-app-api/src/test/java/com/tacit/app/service/impl/UserServiceImplTest.java b/tacit-app-api/src/test/java/com/tacit/app/service/impl/UserServiceImplTest.java new file mode 100644 index 0000000..7fdc4b2 --- /dev/null +++ b/tacit-app-api/src/test/java/com/tacit/app/service/impl/UserServiceImplTest.java @@ -0,0 +1,196 @@ +package com.tacit.app.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.tacit.app.entity.User; +import com.tacit.app.entity.dto.LoginRequest; +import com.tacit.app.entity.dto.LoginResponse; +import com.tacit.app.mapper.UserMapper; +import com.tacit.common.constant.CommonConstant; +import com.tacit.common.exception.BusinessException; +import com.tacit.common.utils.JwtUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * UserServiceImpl单元测试 + */ +@ExtendWith(MockitoExtension.class) +public class UserServiceImplTest { + + @InjectMocks + private UserServiceImpl userService; + + @Mock + private UserMapper userMapper; + + @Mock + private PasswordEncoder passwordEncoder; + + private User testUser; + private LoginRequest testLoginRequest; + + @BeforeEach + public void setUp() { + // 初始化测试用户 + testUser = new User(); + testUser.setId(1L); + testUser.setUsername("testuser"); + testUser.setPassword("encryptedPassword"); + testUser.setRole(CommonConstant.ROLE_USER); + testUser.setStatus(1); + testUser.setDelFlag(0); + + // 初始化登录请求 + testLoginRequest = new LoginRequest(); + testLoginRequest.setUsername("testuser"); + testLoginRequest.setPassword("password123"); + } + + @Test + public void testLoginSuccess() { + // 模拟查询用户 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + // 模拟密码验证 + when(passwordEncoder.matches(testLoginRequest.getPassword(), testUser.getPassword())).thenReturn(true); + + // 执行登录 + LoginResponse loginResponse = userService.login(testLoginRequest); + + // 验证结果 + assertNotNull(loginResponse); + assertNotNull(loginResponse.getToken()); + assertEquals(testUser, loginResponse.getUser()); + verify(userMapper, times(1)).selectOne(any(QueryWrapper.class)); + verify(passwordEncoder, times(1)).matches(anyString(), anyString()); + } + + @Test + public void testLoginUserNotFound() { + // 模拟用户不存在 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(null); + + // 验证抛出异常 + BusinessException exception = assertThrows(BusinessException.class, () -> { + userService.login(testLoginRequest); + }); + assertEquals("用户名或密码错误", exception.getMessage()); + verify(userMapper, times(1)).selectOne(any(QueryWrapper.class)); + } + + @Test + public void testLoginUserDisabled() { + // 设置用户为禁用状态 + testUser.setStatus(0); + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + + // 验证抛出异常 + BusinessException exception = assertThrows(BusinessException.class, () -> { + userService.login(testLoginRequest); + }); + assertEquals("用户已禁用", exception.getMessage()); + } + + @Test + public void testLoginPasswordError() { + // 模拟密码验证失败 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + when(passwordEncoder.matches(testLoginRequest.getPassword(), testUser.getPassword())).thenReturn(false); + + // 验证抛出异常 + BusinessException exception = assertThrows(BusinessException.class, () -> { + userService.login(testLoginRequest); + }); + assertEquals("用户名或密码错误", exception.getMessage()); + } + + @Test + public void testRegisterSuccess() { + // 模拟用户名不存在 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(null); + // 模拟密码加密 + when(passwordEncoder.encode(testUser.getPassword())).thenReturn("encryptedPassword"); + // 模拟保存成功 + when(userService.save(testUser)).thenReturn(true); + + // 执行注册 + boolean result = userService.register(testUser); + + // 验证结果 + assertTrue(result); + assertEquals(CommonConstant.ROLE_USER, testUser.getRole()); + assertEquals(1, testUser.getStatus()); + assertEquals(0, testUser.getDelFlag()); + verify(userMapper, times(1)).selectOne(any(QueryWrapper.class)); + verify(passwordEncoder, times(1)).encode(anyString()); + verify(userService, times(1)).save(testUser); + } + + @Test + public void testRegisterUsernameExists() { + // 模拟用户名已存在 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + + // 验证抛出异常 + BusinessException exception = assertThrows(BusinessException.class, () -> { + userService.register(testUser); + }); + assertEquals("用户名已存在", exception.getMessage()); + } + + @Test + public void testGetUserInfo() { + // 模拟查询用户 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + + // 执行查询 + User user = userService.getUserInfo(1L); + + // 验证结果 + assertNotNull(user); + assertEquals(testUser.getId(), user.getId()); + verify(userMapper, times(1)).selectOne(any(QueryWrapper.class)); + } + + @Test + public void testUpdateUserInfo() { + // 模拟查询用户 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(testUser); + // 模拟更新成功 + when(userService.updateById(testUser)).thenReturn(true); + + // 修改用户信息 + testUser.setNickname("新昵称"); + testUser.setEmail("new@example.com"); + + // 执行更新 + boolean result = userService.updateUserInfo(testUser); + + // 验证结果 + assertTrue(result); + assertEquals("新昵称", testUser.getNickname()); + assertEquals("new@example.com", testUser.getEmail()); + verify(userMapper, times(1)).selectOne(any(QueryWrapper.class)); + verify(userService, times(1)).updateById(testUser); + } + + @Test + public void testUpdateUserInfoUserNotFound() { + // 模拟用户不存在 + when(userMapper.selectOne(any(QueryWrapper.class))).thenReturn(null); + + // 验证抛出异常 + BusinessException exception = assertThrows(BusinessException.class, () -> { + userService.updateUserInfo(testUser); + }); + assertEquals("用户不存在", exception.getMessage()); + } +} diff --git a/tacit-app-api/target/classes/application-dev.yml b/tacit-app-api/target/classes/application-dev.yml new file mode 100644 index 0000000..543bc32 --- /dev/null +++ b/tacit-app-api/target/classes/application-dev.yml @@ -0,0 +1,30 @@ +server: + port: 8082 + +spring: + datasource: + driver-class-name: com.oceanbase.jdbc.Driver + url: jdbc:oceanbase://localhost:2881/tacit?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: password + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:mapper/**/*.xml + type-aliases-package: com.tacit.app.entity + +# Swagger Configuration +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + +# Logging Configuration +logging: + level: + com.tacit.app: debug diff --git a/tacit-app-api/target/classes/bootstrap.yml b/tacit-app-api/target/classes/bootstrap.yml new file mode 100644 index 0000000..ad0ac67 --- /dev/null +++ b/tacit-app-api/target/classes/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-app-api + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-app-api/target/classes/com/tacit/app/AppApiApplication.class b/tacit-app-api/target/classes/com/tacit/app/AppApiApplication.class new file mode 100644 index 0000000..0ad3508 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/AppApiApplication.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/config/SwaggerConfig.class b/tacit-app-api/target/classes/com/tacit/app/config/SwaggerConfig.class new file mode 100644 index 0000000..3294705 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/config/SwaggerConfig.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/controller/AuthController.class b/tacit-app-api/target/classes/com/tacit/app/controller/AuthController.class new file mode 100644 index 0000000..aefb23d Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/controller/AuthController.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/controller/UserController.class b/tacit-app-api/target/classes/com/tacit/app/controller/UserController.class new file mode 100644 index 0000000..37e41a4 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/controller/UserController.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/entity/User.class b/tacit-app-api/target/classes/com/tacit/app/entity/User.class new file mode 100644 index 0000000..a42f388 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/entity/User.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginRequest.class b/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginRequest.class new file mode 100644 index 0000000..7ca9d4f Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginRequest.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginResponse.class b/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginResponse.class new file mode 100644 index 0000000..a129344 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/entity/dto/LoginResponse.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/mapper/UserMapper.class b/tacit-app-api/target/classes/com/tacit/app/mapper/UserMapper.class new file mode 100644 index 0000000..bc7b27c Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/mapper/UserMapper.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/service/UserService.class b/tacit-app-api/target/classes/com/tacit/app/service/UserService.class new file mode 100644 index 0000000..ead780c Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/service/UserService.class differ diff --git a/tacit-app-api/target/classes/com/tacit/app/service/impl/UserServiceImpl.class b/tacit-app-api/target/classes/com/tacit/app/service/impl/UserServiceImpl.class new file mode 100644 index 0000000..acd38c0 Binary files /dev/null and b/tacit-app-api/target/classes/com/tacit/app/service/impl/UserServiceImpl.class differ diff --git a/tacit-common/pom.xml b/tacit-common/pom.xml new file mode 100644 index 0000000..8b822a7 --- /dev/null +++ b/tacit-common/pom.xml @@ -0,0 +1,98 @@ + + + + tacit-parent + com.tacit + 1.0.0-SNAPSHOT + + 4.0.0 + + tacit-common + Tacit Common + Common module for Tacit microservices + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.oceanbase + oceanbase-client + 2.4.0 + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + true + + + + + \ No newline at end of file diff --git a/tacit-common/src/main/java/com/tacit/common/constant/CommonConstant.java b/tacit-common/src/main/java/com/tacit/common/constant/CommonConstant.java new file mode 100644 index 0000000..b167652 --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/constant/CommonConstant.java @@ -0,0 +1,27 @@ +package com.tacit.common.constant; + +public class CommonConstant { + // 状态常量 + public static final Integer STATUS_ENABLE = 1; + public static final Integer STATUS_DISABLE = 0; + + // 删除状态 + public static final Integer DEL_FLAG_NORMAL = 0; + public static final Integer DEL_FLAG_DELETED = 1; + + // JWT相关常量 + public static final String JWT_HEADER = "Authorization"; + public static final String JWT_PREFIX = "Bearer "; + public static final String JWT_SECRET = "tacit_app_secret_key_2024"; + // 7天 + public static final Long JWT_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; + + // 服务名称常量 + public static final String SERVICE_GATEWAY = "tacit-gateway"; + public static final String SERVICE_ADMIN = "tacit-admin"; + public static final String SERVICE_APP_API = "tacit-app-api"; + + // 角色常量 + public static final String ROLE_ADMIN = "admin"; + public static final String ROLE_USER = "user"; +} diff --git a/tacit-common/src/main/java/com/tacit/common/entity/ResponseResult.java b/tacit-common/src/main/java/com/tacit/common/entity/ResponseResult.java new file mode 100644 index 0000000..4298c3a --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/entity/ResponseResult.java @@ -0,0 +1,47 @@ +package com.tacit.common.entity; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class ResponseResult implements Serializable { + private static final long serialVersionUID = 1L; + + private Integer code; + private String message; + private T data; + + private ResponseResult() { + } + + public ResponseResult(Integer code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static ResponseResult success() { + return new ResponseResult<>(200, "操作成功", null); + } + + public static ResponseResult success(T data) { + return new ResponseResult<>(200, "操作成功", data); + } + + public static ResponseResult success(String message, T data) { + return new ResponseResult<>(200, message, data); + } + + public static ResponseResult fail() { + return new ResponseResult<>(500, "操作失败", null); + } + + public static ResponseResult fail(String message) { + return new ResponseResult<>(500, message, null); + } + + public static ResponseResult fail(Integer code, String message) { + return new ResponseResult<>(code, message, null); + } +} diff --git a/tacit-common/src/main/java/com/tacit/common/exception/BusinessException.java b/tacit-common/src/main/java/com/tacit/common/exception/BusinessException.java new file mode 100644 index 0000000..55f7efa --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/exception/BusinessException.java @@ -0,0 +1,34 @@ +package com.tacit.common.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private Integer code; + + public BusinessException() { + super(); + } + + public BusinessException(String message) { + super(message); + this.code = 500; + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = 500; + } + + public BusinessException(Integer code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } +} diff --git a/tacit-common/src/main/java/com/tacit/common/feign/AdminFeignClient.java b/tacit-common/src/main/java/com/tacit/common/feign/AdminFeignClient.java new file mode 100644 index 0000000..c488c48 --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/feign/AdminFeignClient.java @@ -0,0 +1,13 @@ +package com.tacit.common.feign; + +import com.tacit.common.entity.ResponseResult; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "tacit-admin", contextId = "adminFeignClient") +public interface AdminFeignClient { + + @GetMapping("/user/info/{id}") + ResponseResult getUserById(@PathVariable("id") Long id); +} diff --git a/tacit-common/src/main/java/com/tacit/common/feign/AppApiFeignClient.java b/tacit-common/src/main/java/com/tacit/common/feign/AppApiFeignClient.java new file mode 100644 index 0000000..de11dda --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/feign/AppApiFeignClient.java @@ -0,0 +1,13 @@ +package com.tacit.common.feign; + +import com.tacit.common.entity.ResponseResult; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "tacit-app-api", contextId = "appApiFeignClient") +public interface AppApiFeignClient { + + @GetMapping("/user/info/{userId}") + ResponseResult getUserInfo(@PathVariable Long userId); +} diff --git a/tacit-common/src/main/java/com/tacit/common/handler/GlobalExceptionHandler.java b/tacit-common/src/main/java/com/tacit/common/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..e0e64ad --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/handler/GlobalExceptionHandler.java @@ -0,0 +1,72 @@ +package com.tacit.common.handler; + +import com.tacit.common.entity.ResponseResult; +import com.tacit.common.exception.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.util.Objects; + +@ControllerAdvice +@ResponseBody +@Slf4j +public class GlobalExceptionHandler { + + /** + * 处理业务异常 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseResult handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage()); + return ResponseResult.fail(e.getCode() != null ? e.getCode() : 400, e.getMessage()); + } + + /** + * 处理请求参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("请求参数校验异常: {}", e.getMessage()); + String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage(); + return ResponseResult.fail(400, message); + } + + /** + * 处理请求参数类型不匹配异常 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + log.error("请求参数类型不匹配异常: {}", e.getMessage()); + return ResponseResult.fail(400, "请求参数类型不匹配"); + } + + /** + * 处理JSON解析异常 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.error("JSON解析异常: {}", e.getMessage()); + return ResponseResult.fail(400, "JSON格式错误"); + } + + /** + * 处理其他异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseResult handleException(Exception e) { + log.error("系统异常: {}", e.getMessage(), e); + return ResponseResult.fail(500, "系统内部错误"); + } +} diff --git a/tacit-common/src/main/java/com/tacit/common/utils/JwtUtils.java b/tacit-common/src/main/java/com/tacit/common/utils/JwtUtils.java new file mode 100644 index 0000000..5e784a0 --- /dev/null +++ b/tacit-common/src/main/java/com/tacit/common/utils/JwtUtils.java @@ -0,0 +1,112 @@ +package com.tacit.common.utils; + +import com.tacit.common.constant.CommonConstant; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Map; + +@Slf4j +public class JwtUtils { + + private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(CommonConstant.JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + + /** + * 生成JWT令牌 + * @param claims 自定义声明 + * @return JWT令牌 + */ + public static String generateToken(Map claims) { + Date now = new Date(); + Date expireDate = new Date(now.getTime() + CommonConstant.JWT_EXPIRE_TIME); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expireDate) + .signWith(SECRET_KEY, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 解析JWT令牌 + * @param token JWT令牌 + * @return 自定义声明 + */ + public static Claims parseToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(SECRET_KEY) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + log.error("JWT令牌已过期: {}", e.getMessage()); + throw new RuntimeException("令牌已过期", e); + } catch (UnsupportedJwtException e) { + log.error("不支持的JWT令牌: {}", e.getMessage()); + throw new RuntimeException("不支持的令牌格式", e); + } catch (MalformedJwtException e) { + log.error("JWT令牌格式错误: {}", e.getMessage()); + throw new RuntimeException("令牌格式错误", e); + } catch (SecurityException e) { + log.error("JWT令牌验证失败: {}", e.getMessage()); + throw new RuntimeException("令牌验证失败", e); + } catch (Exception e) { + log.error("JWT令牌解析异常: {}", e.getMessage()); + throw new RuntimeException("令牌解析异常", e); + } + } + + /** + * 从JWT令牌中获取用户ID + * @param token JWT令牌 + * @return 用户ID + */ + public static Long getUserIdFromToken(String token) { + Claims claims = parseToken(token); + return Long.parseLong(claims.get("userId").toString()); + } + + /** + * 从JWT令牌中获取用户名 + * @param token JWT令牌 + * @return 用户名 + */ + public static String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + return claims.get("username").toString(); + } + + /** + * 从JWT令牌中获取角色 + * @param token JWT令牌 + * @return 角色 + */ + public static String getRoleFromToken(String token) { + Claims claims = parseToken(token); + return claims.get("role").toString(); + } + + /** + * 验证JWT令牌是否有效 + * @param token JWT令牌 + * @return 是否有效 + */ + public static boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(SECRET_KEY) + .build() + .parseClaimsJws(token); + return true; + } catch (Exception e) { + log.error("JWT令牌验证失败: {}", e.getMessage()); + return false; + } + } +} diff --git a/tacit-common/src/test/java/com/tacit/common/entity/ResponseResultTest.java b/tacit-common/src/test/java/com/tacit/common/entity/ResponseResultTest.java new file mode 100644 index 0000000..789621a --- /dev/null +++ b/tacit-common/src/test/java/com/tacit/common/entity/ResponseResultTest.java @@ -0,0 +1,82 @@ +package com.tacit.common.entity; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * ResponseResult类单元测试 + */ +public class ResponseResultTest { + + @Test + public void testSuccessWithData() { + String data = "test data"; + ResponseResult result = ResponseResult.success(data); + + assertEquals(200, result.getCode()); + assertEquals("操作成功", result.getMessage()); + assertEquals(data, result.getData()); + } + + @Test + public void testSuccessWithoutData() { + ResponseResult result = ResponseResult.success(); + + assertEquals(200, result.getCode()); + assertEquals("操作成功", result.getMessage()); + assertNull(result.getData()); + } + + @Test + public void testFailWithMessage() { + String message = "自定义错误消息"; + ResponseResult result = ResponseResult.fail(message); + + assertEquals(500, result.getCode()); + assertEquals(message, result.getMessage()); + assertNull(result.getData()); + } + + @Test + public void testFailDefault() { + ResponseResult result = ResponseResult.fail(); + + assertEquals(500, result.getCode()); + assertEquals("操作失败", result.getMessage()); + assertNull(result.getData()); + } + + @Test + public void testFailWithCodeAndMessage() { + int code = 400; + String message = "参数错误"; + ResponseResult result = ResponseResult.fail(code, message); + + assertFalse(result.isSuccess()); + assertEquals(code, result.getCode()); + assertEquals(message, result.getMessage()); + assertNull(result.getData()); + } + + @Test + public void testEqualsAndHashCode() { + ResponseResult result1 = ResponseResult.success("test"); + ResponseResult result2 = ResponseResult.success("test"); + ResponseResult result3 = ResponseResult.success("different"); + + assertEquals(result1, result2); + assertNotEquals(result1, result3); + assertEquals(result1.hashCode(), result2.hashCode()); + } + + @Test + public void testToString() { + ResponseResult result = ResponseResult.success("test"); + String toString = result.toString(); + + assertNotNull(toString); + assertTrue(toString.contains("success")); + assertTrue(toString.contains("test")); + } +} diff --git a/tacit-common/src/test/java/com/tacit/common/utils/JwtUtilsTest.java b/tacit-common/src/test/java/com/tacit/common/utils/JwtUtilsTest.java new file mode 100644 index 0000000..d10f644 --- /dev/null +++ b/tacit-common/src/test/java/com/tacit/common/utils/JwtUtilsTest.java @@ -0,0 +1,75 @@ +package com.tacit.common.utils; + +import com.tacit.common.constant.CommonConstant; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JWT工具类单元测试 + */ +public class JwtUtilsTest { + + private String token; + private Map claims; + + @BeforeEach + public void setUp() { + claims = new HashMap<>(); + claims.put("userId", 1L); + claims.put("username", "testuser"); + claims.put("role", "admin"); + token = JwtUtils.generateToken(claims); + } + + @Test + public void testGenerateToken() { + assertNotNull(token); + assertTrue(token.length() > 0); + assertTrue(token.contains(".")); + } + + @Test + public void testParseToken() { + Claims parsedClaims = JwtUtils.parseToken(token); + assertNotNull(parsedClaims); + assertEquals(1L, parsedClaims.get("userId")); + assertEquals("testuser", parsedClaims.get("username")); + assertEquals("admin", parsedClaims.get("role")); + } + + @Test + public void testValidateToken() { + boolean isValid = JwtUtils.validateToken(token); + assertTrue(isValid); + } + + @Test + public void testValidateTokenWithInvalidToken() { + boolean isValid = JwtUtils.validateToken("invalid.token.here"); + assertFalse(isValid); + } + + @Test + public void testGetUserIdFromToken() { + Long userId = JwtUtils.getUserIdFromToken(token); + assertEquals(1L, userId); + } + + @Test + public void testGetUsernameFromToken() { + String username = JwtUtils.getUsernameFromToken(token); + assertEquals("testuser", username); + } + + @Test + public void testGetRoleFromToken() { + String role = JwtUtils.getRoleFromToken(token); + assertEquals("admin", role); + } +} diff --git a/tacit-common/target/classes/com/tacit/common/constant/CommonConstant.class b/tacit-common/target/classes/com/tacit/common/constant/CommonConstant.class new file mode 100644 index 0000000..f5abb21 Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/constant/CommonConstant.class differ diff --git a/tacit-common/target/classes/com/tacit/common/entity/ResponseResult.class b/tacit-common/target/classes/com/tacit/common/entity/ResponseResult.class new file mode 100644 index 0000000..769a915 Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/entity/ResponseResult.class differ diff --git a/tacit-common/target/classes/com/tacit/common/exception/BusinessException.class b/tacit-common/target/classes/com/tacit/common/exception/BusinessException.class new file mode 100644 index 0000000..d19952e Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/exception/BusinessException.class differ diff --git a/tacit-common/target/classes/com/tacit/common/feign/AdminFeignClient.class b/tacit-common/target/classes/com/tacit/common/feign/AdminFeignClient.class new file mode 100644 index 0000000..dc3cf8b Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/feign/AdminFeignClient.class differ diff --git a/tacit-common/target/classes/com/tacit/common/feign/AppApiFeignClient.class b/tacit-common/target/classes/com/tacit/common/feign/AppApiFeignClient.class new file mode 100644 index 0000000..50c6877 Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/feign/AppApiFeignClient.class differ diff --git a/tacit-common/target/classes/com/tacit/common/handler/GlobalExceptionHandler.class b/tacit-common/target/classes/com/tacit/common/handler/GlobalExceptionHandler.class new file mode 100644 index 0000000..63cd5ba Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/handler/GlobalExceptionHandler.class differ diff --git a/tacit-common/target/classes/com/tacit/common/utils/JwtUtils.class b/tacit-common/target/classes/com/tacit/common/utils/JwtUtils.class new file mode 100644 index 0000000..86be20c Binary files /dev/null and b/tacit-common/target/classes/com/tacit/common/utils/JwtUtils.class differ diff --git a/tacit-gateway/pom.xml b/tacit-gateway/pom.xml new file mode 100644 index 0000000..222831d --- /dev/null +++ b/tacit-gateway/pom.xml @@ -0,0 +1,79 @@ + + + + tacit-parent + com.tacit + 1.0.0-SNAPSHOT + + 4.0.0 + + tacit-gateway + Tacit Gateway + Gateway Service for Tacit Microservices + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.tacit + tacit-common + ${project.parent.version} + + + + + org.projectlombok + lombok + provided + + + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + + org.springframework.boot + spring-boot-starter-logging + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + false + + + + + \ No newline at end of file diff --git a/tacit-gateway/src/main/java/com/tacit/gateway/GatewayApplication.java b/tacit-gateway/src/main/java/com/tacit/gateway/GatewayApplication.java new file mode 100644 index 0000000..76f7e76 --- /dev/null +++ b/tacit-gateway/src/main/java/com/tacit/gateway/GatewayApplication.java @@ -0,0 +1,13 @@ +package com.tacit.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } +} diff --git a/tacit-gateway/src/main/java/com/tacit/gateway/filter/JwtAuthenticationFilter.java b/tacit-gateway/src/main/java/com/tacit/gateway/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..cf0b9c6 --- /dev/null +++ b/tacit-gateway/src/main/java/com/tacit/gateway/filter/JwtAuthenticationFilter.java @@ -0,0 +1,104 @@ +package com.tacit.gateway.filter; + +import com.tacit.common.constant.CommonConstant; +import com.tacit.common.utils.JwtUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +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.util.List; + +@Component +@Slf4j +public class JwtAuthenticationFilter implements GlobalFilter, Ordered { + + // 不需要认证的路径 + private static final List WHITE_LIST = List.of( + "/api/auth/login", + "/api/auth/register", + "/swagger-ui", + "/v3/api-docs" + ); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getURI().getPath(); + + // 检查是否在白名单中 + if (isWhiteList(path)) { + return chain.filter(exchange); + } + + // 获取Authorization头 + HttpHeaders headers = request.getHeaders(); + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + + // 检查Authorization头是否存在 + if (StringUtils.isBlank(authorization)) { + return unauthorizedResponse(exchange, "缺少认证令牌"); + } + + // 检查Authorization头格式 + if (!authorization.startsWith(CommonConstant.JWT_PREFIX)) { + return unauthorizedResponse(exchange, "认证令牌格式错误"); + } + + // 提取JWT令牌 + String token = authorization.substring(CommonConstant.JWT_PREFIX.length()); + + // 验证JWT令牌 + try { + JwtUtils.validateToken(token); + + // 从令牌中获取用户信息并添加到请求头 + Long userId = JwtUtils.getUserIdFromToken(token); + String username = JwtUtils.getUsernameFromToken(token); + String role = JwtUtils.getRoleFromToken(token); + + // 将用户信息添加到请求头 + ServerHttpRequest mutatedRequest = request.mutate() + .header("X-User-Id", String.valueOf(userId)) + .header("X-Username", username) + .header("X-Role", role) + .build(); + + return chain.filter(exchange.mutate().request(mutatedRequest).build()); + } catch (Exception e) { + log.error("JWT认证失败: {}", e.getMessage()); + return unauthorizedResponse(exchange, e.getMessage()); + } + } + + /** + * 检查路径是否在白名单中 + */ + private boolean isWhiteList(String path) { + return WHITE_LIST.stream().anyMatch(path::startsWith); + } + + /** + * 返回未授权响应 + */ + private Mono unauthorizedResponse(ServerWebExchange exchange, String message) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json"); + String responseBody = String.format("{\"code\": 401, \"message\": \"%s\"}", message); + return response.writeWith(Mono.just(response.bufferFactory().wrap(responseBody.getBytes()))); + } + + @Override + public int getOrder() { + return -100; + } +} diff --git a/tacit-gateway/src/main/resources/application-dev.yml b/tacit-gateway/src/main/resources/application-dev.yml new file mode 100644 index 0000000..df67938 --- /dev/null +++ b/tacit-gateway/src/main/resources/application-dev.yml @@ -0,0 +1,50 @@ +server: + port: 8080 + +spring: + cloud: + gateway: + discovery: + locator: + enabled: true + lower-case-service-id: true + routes: + # Admin Service Route + - id: tacit-admin + uri: lb://tacit-admin + predicates: + - Path=/admin/** + filters: + - StripPrefix=1 + + # App API Service Route + - id: tacit-app-api + uri: lb://tacit-app-api + predicates: + - Path=/api/** + filters: + - StripPrefix=1 + - JwtAuthenticationFilter + + # Swagger UI Routes + - id: swagger-admin + uri: lb://tacit-admin + predicates: + - Path=/swagger-admin/** + filters: + - StripPrefix=1 + + - id: swagger-app-api + uri: lb://tacit-app-api + predicates: + - Path=/swagger-app-api/** + filters: + - StripPrefix=1 + +# Logging Configuration +logging: + level: + org.springframework.cloud.gateway: debug + org.springframework.http.server.reactive: debug + org.springframework.web.reactive: debug + reactor.netty: debug diff --git a/tacit-gateway/src/main/resources/bootstrap.yml b/tacit-gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..0df105b --- /dev/null +++ b/tacit-gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-gateway + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-gateway/target/classes/application-dev.yml b/tacit-gateway/target/classes/application-dev.yml new file mode 100644 index 0000000..df67938 --- /dev/null +++ b/tacit-gateway/target/classes/application-dev.yml @@ -0,0 +1,50 @@ +server: + port: 8080 + +spring: + cloud: + gateway: + discovery: + locator: + enabled: true + lower-case-service-id: true + routes: + # Admin Service Route + - id: tacit-admin + uri: lb://tacit-admin + predicates: + - Path=/admin/** + filters: + - StripPrefix=1 + + # App API Service Route + - id: tacit-app-api + uri: lb://tacit-app-api + predicates: + - Path=/api/** + filters: + - StripPrefix=1 + - JwtAuthenticationFilter + + # Swagger UI Routes + - id: swagger-admin + uri: lb://tacit-admin + predicates: + - Path=/swagger-admin/** + filters: + - StripPrefix=1 + + - id: swagger-app-api + uri: lb://tacit-app-api + predicates: + - Path=/swagger-app-api/** + filters: + - StripPrefix=1 + +# Logging Configuration +logging: + level: + org.springframework.cloud.gateway: debug + org.springframework.http.server.reactive: debug + org.springframework.web.reactive: debug + reactor.netty: debug diff --git a/tacit-gateway/target/classes/bootstrap.yml b/tacit-gateway/target/classes/bootstrap.yml new file mode 100644 index 0000000..0df105b --- /dev/null +++ b/tacit-gateway/target/classes/bootstrap.yml @@ -0,0 +1,16 @@ +spring: + application: + name: tacit-gateway + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: public + config: + server-addr: localhost:8848 + namespace: public + file-extension: yml + group: DEFAULT_GROUP + refresh-enabled: true + profiles: + active: dev diff --git a/tacit-gateway/target/classes/com/tacit/gateway/GatewayApplication.class b/tacit-gateway/target/classes/com/tacit/gateway/GatewayApplication.class new file mode 100644 index 0000000..860313e Binary files /dev/null and b/tacit-gateway/target/classes/com/tacit/gateway/GatewayApplication.class differ diff --git a/tacit-gateway/target/classes/com/tacit/gateway/filter/JwtAuthenticationFilter.class b/tacit-gateway/target/classes/com/tacit/gateway/filter/JwtAuthenticationFilter.class new file mode 100644 index 0000000..5171847 Binary files /dev/null and b/tacit-gateway/target/classes/com/tacit/gateway/filter/JwtAuthenticationFilter.class differ