This commit is contained in:
2026-04-14 16:27:47 +08:00
commit 4b38a4c952
134 changed files with 7478 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-api</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,31 @@
package com.k12study.common.api.response;
import com.k12study.common.core.utils.TraceIdHolder;
import lombok.Getter;
@Getter
public class ApiResponse<T> {
private final int code;
private final String message;
private final T data;
private final String traceId;
private ApiResponse(int code, String message, T data, String traceId) {
this.code = code;
this.message = message;
this.data = data;
this.traceId = traceId;
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(0, "OK", data, TraceIdHolder.getOrCreate());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(0, message, data, TraceIdHolder.getOrCreate());
}
public static <T> ApiResponse<T> failure(int code, String message) {
return new ApiResponse<>(code, message, null, TraceIdHolder.getOrCreate());
}
}

View File

@@ -0,0 +1,24 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-core</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.k12study.common.core.constants;
public final class SecurityConstants {
public static final String TRACE_ID = "X-Trace-Id";
public static final String HEADER_USER_ID = "X-User-Id";
public static final String HEADER_USERNAME = "X-Username";
public static final String HEADER_DISPLAY_NAME = "X-Display-Name";
public static final String HEADER_TENANT_ID = "X-Tenant-Id";
public static final String HEADER_DEPT_ID = "X-Dept-Id";
private SecurityConstants() {
}
}

View File

@@ -0,0 +1,11 @@
package com.k12study.common.core.domain;
public record RouteKey(
String provinceCode,
String areaCode,
String tenantId,
String tenantPath,
String deptId,
String deptPath
) {
}

View File

@@ -0,0 +1,27 @@
package com.k12study.common.core.utils;
import java.util.UUID;
public final class TraceIdHolder {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private TraceIdHolder() {
}
public static String getOrCreate() {
String value = TRACE_ID.get();
if (value == null || value.isBlank()) {
value = UUID.randomUUID().toString().replace("-", "");
TRACE_ID.set(value);
}
return value;
}
public static void set(String traceId) {
TRACE_ID.set(traceId);
}
public static void clear() {
TRACE_ID.remove();
}
}

View File

@@ -0,0 +1,28 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-mybatis</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package com.k12study.common.mybatis.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfiguration {
}

View File

@@ -0,0 +1,19 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-redis</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package com.k12study.common.redis.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfiguration {
}

View File

@@ -0,0 +1,38 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-security</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
</dependencies>
</project>

View File

@@ -0,0 +1,82 @@
package com.k12study.common.security.config;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
private boolean enabled = true;
private boolean gatewayMode = false;
private String tokenHeader = "Authorization";
private String tokenPrefix = "Bearer ";
private String secret = "k12study-dev-secret-k12study-dev-secret";
private Duration accessTokenTtl = Duration.ofHours(12);
private Duration refreshTokenTtl = Duration.ofDays(7);
private List<String> whitelist = new ArrayList<>(List.of("/actuator/**", "/auth/login", "/auth/refresh"));
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isGatewayMode() {
return gatewayMode;
}
public void setGatewayMode(boolean gatewayMode) {
this.gatewayMode = gatewayMode;
}
public String getTokenHeader() {
return tokenHeader;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public String getTokenPrefix() {
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Duration getAccessTokenTtl() {
return accessTokenTtl;
}
public void setAccessTokenTtl(Duration accessTokenTtl) {
this.accessTokenTtl = accessTokenTtl;
}
public Duration getRefreshTokenTtl() {
return refreshTokenTtl;
}
public void setRefreshTokenTtl(Duration refreshTokenTtl) {
this.refreshTokenTtl = refreshTokenTtl;
}
public List<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(List<String> whitelist) {
this.whitelist = whitelist;
}
}

View File

@@ -0,0 +1,16 @@
package com.k12study.common.security.config;
import com.k12study.common.security.jwt.JwtTokenProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(AuthProperties.class)
public class SecurityAutoConfiguration {
@Bean
public JwtTokenProvider jwtTokenProvider(AuthProperties authProperties) {
return new JwtTokenProvider(authProperties);
}
}

View File

@@ -0,0 +1,10 @@
package com.k12study.common.security.context;
public record RequestUserContext(
String userId,
String username,
String displayName,
String tenantId,
String deptId
) {
}

View File

@@ -0,0 +1,20 @@
package com.k12study.common.security.context;
public final class RequestUserContextHolder {
private static final ThreadLocal<RequestUserContext> CONTEXT = new ThreadLocal<>();
private RequestUserContextHolder() {
}
public static void set(RequestUserContext context) {
CONTEXT.set(context);
}
public static RequestUserContext get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}

View File

@@ -0,0 +1,49 @@
package com.k12study.common.security.jwt;
import com.k12study.common.security.config.AuthProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import javax.crypto.SecretKey;
public class JwtTokenProvider {
private final AuthProperties authProperties;
private final SecretKey secretKey;
public JwtTokenProvider(AuthProperties authProperties) {
this.authProperties = authProperties;
this.secretKey = Keys.hmacShaKeyFor(authProperties.getSecret().getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(JwtUserPrincipal principal) {
Instant now = Instant.now();
return Jwts.builder()
.subject(principal.userId())
.claim("username", principal.username())
.claim("displayName", principal.displayName())
.claim("tenantId", principal.tenantId())
.claim("deptId", principal.deptId())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(authProperties.getAccessTokenTtl())))
.signWith(secretKey)
.compact();
}
public JwtUserPrincipal parse(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return new JwtUserPrincipal(
claims.getSubject(),
claims.get("username", String.class),
claims.get("displayName", String.class),
claims.get("tenantId", String.class),
claims.get("deptId", String.class)
);
}
}

View File

@@ -0,0 +1,10 @@
package com.k12study.common.security.jwt;
public record JwtUserPrincipal(
String userId,
String username,
String displayName,
String tenantId,
String deptId
) {
}

View File

@@ -0,0 +1,34 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-web</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
package com.k12study.common.web.config;
import com.k12study.common.core.constants.SecurityConstants;
import com.k12study.common.core.utils.TraceIdHolder;
import com.k12study.common.security.context.RequestUserContext;
import com.k12study.common.security.context.RequestUserContextHolder;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
@Configuration
public class CommonWebMvcConfiguration extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String traceId = request.getHeader(SecurityConstants.TRACE_ID);
TraceIdHolder.set(traceId == null || traceId.isBlank() ? TraceIdHolder.getOrCreate() : traceId);
RequestUserContextHolder.set(new RequestUserContext(
request.getHeader(SecurityConstants.HEADER_USER_ID),
request.getHeader(SecurityConstants.HEADER_USERNAME),
request.getHeader(SecurityConstants.HEADER_DISPLAY_NAME),
request.getHeader(SecurityConstants.HEADER_TENANT_ID),
request.getHeader(SecurityConstants.HEADER_DEPT_ID)
));
response.setHeader(SecurityConstants.TRACE_ID, TraceIdHolder.getOrCreate());
filterChain.doFilter(request, response);
} finally {
TraceIdHolder.clear();
RequestUserContextHolder.clear();
}
}
}

View File

@@ -0,0 +1,14 @@
package com.k12study.common.web.exception;
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -0,0 +1,19 @@
package com.k12study.common.web.exception;
import com.k12study.common.api.response.ApiResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ApiResponse<Void> handleBizException(BizException exception) {
return ApiResponse.failure(exception.getCode(), exception.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception exception) {
return ApiResponse.failure(500, exception.getMessage());
}
}

24
backend/common/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>common-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>common-api</module>
<module>common-core</module>
<module>common-web</module>
<module>common-security</module>
<module>common-mybatis</module>
<module>common-redis</module>
</modules>
</project>