This commit is contained in:
2026-04-17 16:31:32 +08:00
parent adadb3bf1d
commit 2476655b28
116 changed files with 3875 additions and 583 deletions

View File

@@ -16,6 +16,21 @@
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -9,7 +9,10 @@ public record CurrentUserResponse(
String provinceCode,
String areaCode,
String tenantId,
String tenantPath,
String deptId,
List<String> roles
String deptPath,
List<String> roles,
String clientType
) {
}

View File

@@ -3,8 +3,11 @@ package com.k12study.api.auth.dto;
public record LoginRequest(
String username,
String password,
String mobile,
String smsCode,
String provinceCode,
String areaCode,
String tenantId
String tenantId,
String clientType
) {
}

View File

@@ -0,0 +1,18 @@
package com.k12study.api.auth.remote;
/**
* @description Auth 模块 HTTP 路径常量,供 Controller/Feign/网关白名单共用,避免字面量漂移
* @filename AuthApiPaths.java
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
public final class AuthApiPaths {
private AuthApiPaths() {
}
public static final String BASE = "/auth";
public static final String LOGIN = BASE + "/tokens";
public static final String REFRESH = BASE + "/tokens/refresh";
public static final String USERS_CURRENT = BASE + "/users/current";
}

View File

@@ -0,0 +1,43 @@
package com.k12study.api.auth.remote;
import com.k12study.api.auth.dto.CurrentUserResponse;
import com.k12study.api.auth.dto.LoginRequest;
import com.k12study.api.auth.dto.RefreshTokenRequest;
import com.k12study.api.auth.dto.TokenResponse;
import com.k12study.api.auth.remote.factory.RemoteAuthServiceFallbackFactory;
import com.k12study.common.api.response.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
/**
* @description Auth 远程服务契约;微服务模式按 Nacos 服务名寻址,单体/本地通过 k12study.remote.auth.url 直连兜底
* @filename AuthRemoteApi.java
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
@FeignClient(
contextId = "remoteAuthService",
value = "${k12study.remote.auth.service-name:k12study-auth}",
url = "${k12study.remote.auth.url:}",
path = AuthApiPaths.BASE,
fallbackFactory = RemoteAuthServiceFallbackFactory.class)
public interface AuthRemoteApi {
/** 账号密码 / 手机号+验证码登录,成功返回 access+refresh 双令牌 */
@PostMapping("/tokens")
ApiResponse<TokenResponse> login(@RequestBody LoginRequest request);
/** 一次一换:撤销旧 refresh、签发新 access+refresh;失败返回 401 */
@PostMapping("/tokens/refresh")
ApiResponse<TokenResponse> refresh(@RequestBody RefreshTokenRequest request);
/** 解析 Authorization 中的 access token 返回当前用户画像(含 roleCodes/clientType) */
@GetMapping("/users/current")
ApiResponse<CurrentUserResponse> currentUser(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
}

View File

@@ -0,0 +1,49 @@
package com.k12study.api.auth.remote.factory;
import com.k12study.api.auth.dto.CurrentUserResponse;
import com.k12study.api.auth.dto.LoginRequest;
import com.k12study.api.auth.dto.RefreshTokenRequest;
import com.k12study.api.auth.dto.TokenResponse;
import com.k12study.api.auth.remote.AuthRemoteApi;
import com.k12study.common.api.response.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* @description Auth Feign 熔断降级工厂;下游不可达时返回 503 + message 说明,前端按统一响应体处理
* @filename RemoteAuthServiceFallbackFactory.java
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
@Component
public class RemoteAuthServiceFallbackFactory implements FallbackFactory<AuthRemoteApi> {
private static final Logger log = LoggerFactory.getLogger(RemoteAuthServiceFallbackFactory.class);
private static final int FALLBACK_CODE = 503;
@Override
public AuthRemoteApi create(Throwable cause) {
String reason = cause == null ? "unknown" : cause.getClass().getSimpleName() + ":" + cause.getMessage();
log.warn("[auth-fallback] remote auth service degraded, cause={}", reason);
String message = "auth 服务暂不可用,已触发降级:" + reason;
return new AuthRemoteApi() {
@Override
public ApiResponse<TokenResponse> login(LoginRequest request) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<TokenResponse> refresh(RefreshTokenRequest request) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<CurrentUserResponse> currentUser(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
};
}
}

View File

@@ -16,6 +16,21 @@
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -0,0 +1,19 @@
package com.k12study.api.upms.dto;
import java.time.Instant;
public record FileMetadataDto(
String fileId,
String mediaType,
String objectKey,
String fileName,
String mimeType,
Long fileSize,
String fileHash,
Integer durationMs,
String uploadedBy,
String tenantId,
String tenantPath,
Instant createdAt
) {
}

View File

@@ -0,0 +1,12 @@
package com.k12study.api.upms.dto;
public record FileUploadRequestDto(
String mediaType,
String objectKey,
String fileName,
String mimeType,
Long fileSize,
String fileHash,
Integer durationMs
) {
}

View File

@@ -0,0 +1,16 @@
package com.k12study.api.upms.dto;
import java.time.Instant;
public record InboxMessageDto(
String messageId,
String messageType,
String bizType,
String title,
String content,
String webJumpUrl,
String readStatus,
Instant readAt,
Instant sendAt
) {
}

View File

@@ -0,0 +1,10 @@
package com.k12study.api.upms.dto;
import java.time.Instant;
public record MessageReadResultDto(
String messageId,
String readStatus,
Instant readAt
) {
}

View File

@@ -0,0 +1,8 @@
package com.k12study.api.upms.dto;
public record SchoolClassCourseDto(
String classId,
String courseId,
String relationStatus
) {
}

View File

@@ -0,0 +1,12 @@
package com.k12study.api.upms.dto;
public record SchoolClassDto(
String classId,
String classCode,
String className,
String gradeCode,
String status,
String tenantId,
String deptId
) {
}

View File

@@ -0,0 +1,15 @@
package com.k12study.api.upms.dto;
import java.time.Instant;
public record SchoolClassMemberDto(
String classId,
String userId,
String username,
String displayName,
String memberRole,
String memberStatus,
Instant joinedAt,
Instant leftAt
) {
}

View File

@@ -10,4 +10,11 @@ public final class UpmsApiPaths {
public static final String AREAS = BASE + "/areas";
public static final String TENANTS = BASE + "/tenants";
public static final String DEPARTMENTS = BASE + "/departments";
public static final String CLASSES = BASE + "/classes";
public static final String CLASS_MEMBERS = BASE + "/classes/{classId}/members";
public static final String CLASS_COURSES = BASE + "/classes/{classId}/courses";
public static final String FILE_UPLOAD = BASE + "/files/upload";
public static final String FILE_BY_ID = BASE + "/files/{fileId}";
public static final String MESSAGE_INBOX = BASE + "/messages/inbox";
public static final String MESSAGE_READ = BASE + "/messages/{messageId}/read";
}

View File

@@ -3,19 +3,91 @@ package com.k12study.api.upms.remote;
import com.k12study.api.upms.dto.AreaNodeDto;
import com.k12study.api.upms.dto.CurrentRouteUserDto;
import com.k12study.api.upms.dto.DeptNodeDto;
import com.k12study.api.upms.dto.FileMetadataDto;
import com.k12study.api.upms.dto.FileUploadRequestDto;
import com.k12study.api.upms.dto.InboxMessageDto;
import com.k12study.api.upms.dto.MessageReadResultDto;
import com.k12study.api.upms.dto.RouteNodeDto;
import com.k12study.api.upms.dto.SchoolClassCourseDto;
import com.k12study.api.upms.dto.SchoolClassDto;
import com.k12study.api.upms.dto.SchoolClassMemberDto;
import com.k12study.api.upms.dto.TenantNodeDto;
import com.k12study.api.upms.remote.factory.RemoteUpmsServiceFallbackFactory;
import com.k12study.common.api.response.ApiResponse;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
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.RequestHeader;
/**
* @description UPMS 远程服务契约;Controller 方法与此接口路径必须一一对应,启动时由 FeignContractValidator 校验
* @filename UpmsRemoteApi.java
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
@FeignClient(
contextId = "remoteUpmsService",
value = "${k12study.remote.upms.service-name:k12study-upms}",
url = "${k12study.remote.upms.url:}",
path = UpmsApiPaths.BASE,
fallbackFactory = RemoteUpmsServiceFallbackFactory.class)
public interface UpmsRemoteApi {
ApiResponse<List<RouteNodeDto>> routes();
ApiResponse<CurrentRouteUserDto> currentUser();
@GetMapping("/routes")
ApiResponse<List<RouteNodeDto>> routes(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
ApiResponse<List<AreaNodeDto>> areas();
@GetMapping("/users/current")
ApiResponse<CurrentRouteUserDto> currentUser(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
ApiResponse<List<TenantNodeDto>> tenants();
@GetMapping("/areas")
ApiResponse<List<AreaNodeDto>> areas(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
ApiResponse<List<DeptNodeDto>> departments();
@GetMapping("/tenants")
ApiResponse<List<TenantNodeDto>> tenants(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
@GetMapping("/departments")
ApiResponse<List<DeptNodeDto>> departments(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
@GetMapping("/classes")
ApiResponse<List<SchoolClassDto>> classes(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
@GetMapping("/classes/{classId}/members")
ApiResponse<List<SchoolClassMemberDto>> classMembers(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
@PathVariable("classId") String classId);
@GetMapping("/classes/{classId}/courses")
ApiResponse<List<SchoolClassCourseDto>> classCourses(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
@PathVariable("classId") String classId);
@PostMapping("/files/upload")
ApiResponse<FileMetadataDto> uploadFile(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
@RequestBody FileUploadRequestDto request);
@GetMapping("/files/{fileId}")
ApiResponse<FileMetadataDto> fileById(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
@PathVariable("fileId") String fileId);
@GetMapping("/messages/inbox")
ApiResponse<List<InboxMessageDto>> inboxMessages(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization);
@PostMapping("/messages/{messageId}/read")
ApiResponse<MessageReadResultDto> readMessage(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
@PathVariable("messageId") String messageId);
}

View File

@@ -0,0 +1,103 @@
package com.k12study.api.upms.remote.factory;
import com.k12study.api.upms.dto.AreaNodeDto;
import com.k12study.api.upms.dto.CurrentRouteUserDto;
import com.k12study.api.upms.dto.DeptNodeDto;
import com.k12study.api.upms.dto.FileMetadataDto;
import com.k12study.api.upms.dto.FileUploadRequestDto;
import com.k12study.api.upms.dto.InboxMessageDto;
import com.k12study.api.upms.dto.MessageReadResultDto;
import com.k12study.api.upms.dto.RouteNodeDto;
import com.k12study.api.upms.dto.SchoolClassCourseDto;
import com.k12study.api.upms.dto.SchoolClassDto;
import com.k12study.api.upms.dto.SchoolClassMemberDto;
import com.k12study.api.upms.dto.TenantNodeDto;
import com.k12study.api.upms.remote.UpmsRemoteApi;
import com.k12study.common.api.response.ApiResponse;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* @description UPMS Feign 熔断降级工厂;下游不可达时返回 503 + message 说明,避免阻塞调用方,前端按统一响应体处理
* @filename RemoteUpmsServiceFallbackFactory.java
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
@Component
public class RemoteUpmsServiceFallbackFactory implements FallbackFactory<UpmsRemoteApi> {
private static final Logger log = LoggerFactory.getLogger(RemoteUpmsServiceFallbackFactory.class);
private static final int FALLBACK_CODE = 503;
@Override
public UpmsRemoteApi create(Throwable cause) {
String reason = cause == null ? "unknown" : cause.getClass().getSimpleName() + ":" + cause.getMessage();
log.warn("[upms-fallback] remote upms service degraded, cause={}", reason);
String message = "upms 服务暂不可用,已触发降级:" + reason;
return new UpmsRemoteApi() {
@Override
public ApiResponse<List<RouteNodeDto>> routes(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<CurrentRouteUserDto> currentUser(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<AreaNodeDto>> areas(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<TenantNodeDto>> tenants(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<DeptNodeDto>> departments(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<SchoolClassDto>> classes(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<SchoolClassMemberDto>> classMembers(String authorization, String classId) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<SchoolClassCourseDto>> classCourses(String authorization, String classId) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<FileMetadataDto> uploadFile(String authorization, FileUploadRequestDto request) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<FileMetadataDto> fileById(String authorization, String fileId) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<List<InboxMessageDto>> inboxMessages(String authorization) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
@Override
public ApiResponse<MessageReadResultDto> readMessage(String authorization, String messageId) {
return ApiResponse.failure(FALLBACK_CODE, message);
}
};
}
}