Initial commit

This commit is contained in:
panxuejie 2025-12-30 11:14:25 +08:00
commit 4fe7ba5d0d
81 changed files with 2461 additions and 0 deletions

150
pom.xml Normal file
View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tacit</groupId>
<artifactId>tacit-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Tacit Parent</name>
<description>Tacit App Microservice Architecture Parent Project</description>
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<lombok.version>1.18.30</lombok.version>
<jjwt.version>0.11.5</jjwt.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>tacit-gateway</module>
<module>tacit-admin</module>
<module>tacit-app-api</module>
<module>tacit-common</module>
</modules>
<repositories>
<repository>
<id>central</id>
<name>Maven Central Repository</name>
<url>https://repo1.maven.org/maven2/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Central Repository</name>
<url>https://repo1.maven.org/maven2/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

101
tacit-admin/pom.xml Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>tacit-parent</artifactId>
<groupId>com.tacit</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tacit-admin</artifactId>
<name>Tacit Admin</name>
<description>Admin Service for Tacit Microservices</description>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.oceanbase</groupId>
<artifactId>oceanbase-client</artifactId>
<version>2.4.0</version>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Common Module -->
<dependency>
<groupId>com.tacit</groupId>
<artifactId>tacit-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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"));
}
}

View File

@ -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<String> hello() {
return ResponseResult.success("Hello from admin service!");
}
@Operation(summary = "测试Feign调用", description = "测试通过Feign调用APP接口服务")
@GetMapping("/feign/{userId}")
public ResponseResult<Object> testFeign(@PathVariable Long userId) {
return appApiFeignClient.getUserInfo(userId);
}
}

View File

@ -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<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
LoginResponse loginResponse = userService.login(loginRequest);
return ResponseResult.success(loginResponse);
}
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
@GetMapping("/list")
@PreAuthorize("hasRole('ADMIN')")
public ResponseResult<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseResult.success(users);
}
@Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户详情")
@GetMapping("/info/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseResult<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseResult.success(user);
}
@Operation(summary = "创建用户", description = "创建新用户")
@PostMapping("/create")
@PreAuthorize("hasRole('ADMIN')")
public ResponseResult<Boolean> createUser(@RequestBody User user) {
boolean result = userService.createUser(user);
return ResponseResult.success(result);
}
@Operation(summary = "更新用户", description = "更新用户信息")
@PutMapping("/update")
@PreAuthorize("hasRole('ADMIN')")
public ResponseResult<Boolean> 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<Boolean> deleteUser(@PathVariable Long id) {
boolean result = userService.deleteUser(id);
return ResponseResult.success(result);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<User> {
}

View File

@ -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<User> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户信息
*/
User getUserByUsername(String username);
/**
* 根据用户ID查询用户
* @param id 用户ID
* @return 用户信息
*/
User getUserById(Long id);
/**
* 获取所有用户列表
* @return 用户列表
*/
List<User> 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);
}

View File

@ -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<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User getUserByUsername(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username)
.eq("del_flag", 0);
return userMapper.selectOne(queryWrapper);
}
@Override
public User getUserById(Long id) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", id)
.eq("del_flag", 0);
return userMapper.selectOne(queryWrapper);
}
@Override
public List<User> getAllUsers() {
QueryWrapper<User> 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<String, Object> 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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

101
tacit-app-api/pom.xml Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>tacit-parent</artifactId>
<groupId>com.tacit</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tacit-app-api</artifactId>
<name>Tacit App API</name>
<description>App API Service for Tacit Microservices</description>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.oceanbase</groupId>
<artifactId>oceanbase-client</artifactId>
<version>2.4.0</version>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Common Module -->
<dependency>
<groupId>com.tacit</groupId>
<artifactId>tacit-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
LoginResponse loginResponse = userService.login(loginRequest);
return ResponseResult.success(loginResponse);
}
@Operation(summary = "用户注册", description = "用户注册接口")
@PostMapping("/register")
public ResponseResult<Boolean> register(@RequestBody User user) {
boolean result = userService.register(user);
return ResponseResult.success(result);
}
@Operation(summary = "测试Feign调用", description = "测试通过Feign调用管理台服务")
@GetMapping("/test-feign/{userId}")
public ResponseResult<Object> testFeign(@PathVariable Long userId) {
return adminFeignClient.getUserById(userId);
}
}

View File

@ -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<User> getUserInfo(@PathVariable Long userId) {
User user = userService.getUserInfo(userId);
return ResponseResult.success(user);
}
@Operation(summary = "更新用户信息", description = "更新用户个人信息")
@PutMapping("/update")
@PreAuthorize("hasRole('USER')")
public ResponseResult<Boolean> updateUserInfo(@RequestBody User user) {
boolean result = userService.updateUserInfo(user);
return ResponseResult.success(result);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
package com.tacit.app.entity.dto;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}

View File

@ -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;
}

View File

@ -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<User> {
}

View File

@ -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<User> {
/**
* 用户登录
* @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);
}

View File

@ -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<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public LoginResponse login(LoginRequest loginRequest) {
// 根据用户名查询用户
QueryWrapper<User> 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<String, Object> 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<User> 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<User> 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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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());
}
}

View File

@ -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

View File

@ -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

98
tacit-common/pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>tacit-parent</artifactId>
<groupId>com.tacit</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tacit-common</artifactId>
<name>Tacit Common</name>
<description>Common module for Tacit microservices</description>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.oceanbase</groupId>
<artifactId>oceanbase-client</artifactId>
<version>2.4.0</version>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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";
}

View File

@ -0,0 +1,47 @@
package com.tacit.common.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResponseResult<T> 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 <T> ResponseResult<T> success() {
return new ResponseResult<>(200, "操作成功", null);
}
public static <T> ResponseResult<T> success(T data) {
return new ResponseResult<>(200, "操作成功", data);
}
public static <T> ResponseResult<T> success(String message, T data) {
return new ResponseResult<>(200, message, data);
}
public static <T> ResponseResult<T> fail() {
return new ResponseResult<>(500, "操作失败", null);
}
public static <T> ResponseResult<T> fail(String message) {
return new ResponseResult<>(500, message, null);
}
public static <T> ResponseResult<T> fail(Integer code, String message) {
return new ResponseResult<>(code, message, null);
}
}

View File

@ -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;
}
}

View File

@ -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<Object> getUserById(@PathVariable("id") Long id);
}

View File

@ -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<Object> getUserInfo(@PathVariable Long userId);
}

View File

@ -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<Object> 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<Object> 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<Object> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("请求参数类型不匹配异常: {}", e.getMessage());
return ResponseResult.fail(400, "请求参数类型不匹配");
}
/**
* 处理JSON解析异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseResult<Object> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.error("JSON解析异常: {}", e.getMessage());
return ResponseResult.fail(400, "JSON格式错误");
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseResult<Object> handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return ResponseResult.fail(500, "系统内部错误");
}
}

View File

@ -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<String, Object> 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;
}
}
}

View File

@ -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<String> result = ResponseResult.success(data);
assertEquals(200, result.getCode());
assertEquals("操作成功", result.getMessage());
assertEquals(data, result.getData());
}
@Test
public void testSuccessWithoutData() {
ResponseResult<Object> result = ResponseResult.success();
assertEquals(200, result.getCode());
assertEquals("操作成功", result.getMessage());
assertNull(result.getData());
}
@Test
public void testFailWithMessage() {
String message = "自定义错误消息";
ResponseResult<Object> result = ResponseResult.fail(message);
assertEquals(500, result.getCode());
assertEquals(message, result.getMessage());
assertNull(result.getData());
}
@Test
public void testFailDefault() {
ResponseResult<Object> result = ResponseResult.fail();
assertEquals(500, result.getCode());
assertEquals("操作失败", result.getMessage());
assertNull(result.getData());
}
@Test
public void testFailWithCodeAndMessage() {
int code = 400;
String message = "参数错误";
ResponseResult<Object> result = ResponseResult.fail(code, message);
assertFalse(result.isSuccess());
assertEquals(code, result.getCode());
assertEquals(message, result.getMessage());
assertNull(result.getData());
}
@Test
public void testEqualsAndHashCode() {
ResponseResult<String> result1 = ResponseResult.success("test");
ResponseResult<String> result2 = ResponseResult.success("test");
ResponseResult<String> result3 = ResponseResult.success("different");
assertEquals(result1, result2);
assertNotEquals(result1, result3);
assertEquals(result1.hashCode(), result2.hashCode());
}
@Test
public void testToString() {
ResponseResult<String> result = ResponseResult.success("test");
String toString = result.toString();
assertNotNull(toString);
assertTrue(toString.contains("success"));
assertTrue(toString.contains("test"));
}
}

View File

@ -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<String, Object> 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);
}
}

79
tacit-gateway/pom.xml Normal file
View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>tacit-parent</artifactId>
<groupId>com.tacit</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tacit-gateway</artifactId>
<name>Tacit Gateway</name>
<description>Gateway Service for Tacit Microservices</description>
<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Common Module -->
<dependency>
<groupId>com.tacit</groupId>
<artifactId>tacit-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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<String> WHITE_LIST = List.of(
"/api/auth/login",
"/api/auth/register",
"/swagger-ui",
"/v3/api-docs"
);
@Override
public Mono<Void> 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<Void> 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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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