perf(system): 项目授权校验改单行查询 + AuthController 切 FromCache

背景:feat/multi-tenant-project 联调发现 /admin-api/** 全线变慢,
尤其 get-permission-info 经常超时。根因是 ProjectSecurityWebFilter
每请求需校验"用户对项目是否授权",原实现走"拿全量 authorizedIds 再
contains"路径,超管分支还得 selectList 全表项目。Framework 层虽有
60s 本地缓存,cache miss 时仍要走 Feign HTTP 自调用 + 两次 DB。

优化:
1. 新增 ProjectService.isProjectAuthorized(userId, projectId) 单项校验:
   - 超管直通返回 true(不查任何表)
   - 普通用户走 (user_id, project_id) 唯一索引的 selectCount 单行查询
2. ProjectCommonApi / ProjectApiImpl / ProjectFrameworkService(Impl)
   全链路新增 isProjectAuthorized Feign 接口
3. ProjectFrameworkServiceImpl 为 isProjectAuthorized 加 60s 本地
   Guava 缓存(key=(userId,projectId));invalidateAuthorizedProjectCache
   同步清理本用户所有条目
4. ProjectSecurityWebFilter 改调 isProjectAuthorized,消除每请求
   拉全量列表的开销
5. ProjectServiceImpl.getDefaultProjectId 的 N 次 selectById
   改成一次 selectByIds 批量
6. AuthController.getPermissionInfo 第 107 行
   getUserRoleIdListByUserId → FromCache(yudao 原生小瑕疵顺手修)

预期收益:
- Filter 热路径在 cache 命中时 0 次 DB,cache miss 时 1 次单行查询
- get-permission-info 消除一次无缓存 user_role DB 查询

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-23 16:20:29 +08:00
parent 317f1cd02f
commit 9784d7dd8e
8 changed files with 115 additions and 10 deletions

View File

@@ -35,4 +35,11 @@ public interface ProjectCommonApi {
@Parameter(name = "userId", description = "用户编号", required = true)
CommonResult<Long> getDefaultProjectId(@RequestParam("userId") Long userId);
@GetMapping(PREFIX + "/is-authorized")
@Operation(summary = "校验该用户是否有权访问该项目")
@Parameter(name = "userId", description = "用户编号", required = true)
@Parameter(name = "projectId", description = "项目编号", required = true)
CommonResult<Boolean> isProjectAuthorized(@RequestParam("userId") Long userId,
@RequestParam("projectId") Long projectId);
}

View File

@@ -103,9 +103,8 @@ public class ProjectSecurityWebFilter extends ApiRequestFilter {
return;
}
} else {
// 4. 请求已携带 project-id校验用户是否有权限访问该项目
List<Long> authorizedProjectIds = projectFrameworkService.getAuthorizedProjectIds(user.getId());
if (!authorizedProjectIds.contains(projectId)) {
// 4. 请求已携带 project-id单项校验授权(超管直通;普通用户走主键索引)
if (!projectFrameworkService.isProjectAuthorized(user.getId(), projectId)) {
log.error("[doFilterInternal][用户({}) 无权访问项目({})URL({}/{})]",
user.getId(), projectId, request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(

View File

@@ -39,6 +39,15 @@ public interface ProjectFrameworkService {
*/
Long getDefaultProjectId(Long userId);
/**
* 单项校验:该用户是否有权访问该项目(带本地短缓存)
*
* @param userId 用户编号
* @param projectId 项目编号
* @return 是否授权
*/
boolean isProjectAuthorized(Long userId, Long projectId);
/**
* 清除用户的授权项目缓存(授权变更时调用)
*

View File

@@ -9,6 +9,7 @@ import lombok.SneakyThrows;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import static com.viewsh.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
@@ -89,9 +90,42 @@ public class ProjectFrameworkServiceImpl implements ProjectFrameworkService {
return projectApi.getDefaultProjectId(userId).getCheckedData();
}
/**
* 针对 {@link #isProjectAuthorized(Long, Long)} 的本地缓存
* Key: (userId, projectId), Value: 是否授权
* TTL: 60 秒(授权变更后最长 1 分钟生效;写入点可调 invalidateAuthorizedProjectCache 提前生效)
*/
private final LoadingCache<AuthKey, Boolean> projectAuthorizedCache = buildAsyncReloadingCache(
Duration.ofSeconds(60L),
new CacheLoader<AuthKey, Boolean>() {
@Override
public Boolean load(AuthKey key) {
return projectApi.isProjectAuthorized(key.userId(), key.projectId()).getCheckedData();
}
});
@Override
@SneakyThrows
public boolean isProjectAuthorized(Long userId, Long projectId) {
if (userId == null || projectId == null) {
return false;
}
return Boolean.TRUE.equals(projectAuthorizedCache.get(new AuthKey(userId, projectId)));
}
@Override
public void invalidateAuthorizedProjectCache(Long userId) {
authorizedProjectCache.invalidate(userId);
// 同步清 isProjectAuthorized 中属于该用户的所有缓存条目
projectAuthorizedCache.asMap().keySet().removeIf(k -> Objects.equals(k.userId(), userId));
}
/**
* 缓存 key(userId, projectId)
*/
private record AuthKey(Long userId, Long projectId) {
}
}