Browse Source

feat(core): 新增租户上下文与 404 异常,缓存键增加租户维度

- TokenData 新增 currentTenantId()/currentAppCode():A 端取登录租户,
  B 端及无请求上下文(MQ、定时任务)返回 null
- 新增 DataNotFoundException,MyExceptionHandler 统一映射为 HTTP 404 +
  DATA_NOT_EXIST,避免用 403 暴露资源是否存在
- CacheKey 的 FORM_HEAD_TABLE / FORM_HEAD_BY_TABLE_ID /
  FORM_HEAD_TABLE_NAME / FORM_CHILD_FIELD / FORM_ONETOMANY_TABLE /
  FORM_FIELD_BY_HEAD_Id 增加租户段(A 端 tenantId,B 端 base),
  避免管理端复制来的相同 id 在租户之间互相覆盖缓存
feature/2026-10/tenant-multitenant
lml 1 day ago
parent
commit
6907bf5056
  1. 14
      common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java
  2. 23
      common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java
  3. 20
      common/common-core/src/main/java/apelet/common/core/exception/DataNotFoundException.java
  4. 21
      common/common-core/src/main/java/apelet/common/core/object/TokenData.java

14
common/common-core/src/main/java/apelet/common/core/advice/MyExceptionHandler.java

@ -28,6 +28,20 @@ import java.util.concurrent.TimeoutException;
public class MyExceptionHandler { public class MyExceptionHandler {
/** /**
* 数据不存在或不属于当前租户:统一返回 404,避免越权探测(PRD FR-4.16)。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = DataNotFoundException.class)
public ResponseResult<Void> dataNotFoundExceptionHandle(DataNotFoundException ex, HttpServletRequest request) {
log.warn("Data not found or not owned by current tenant from URL [{}]", request.getRequestURI());
ContextUtil.getHttpResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
/**
* 通用异常处理方法。 * 通用异常处理方法。
* *
* @param ex 异常对象。 * @param ex 异常对象。

23
common/common-core/src/main/java/apelet/common/core/cache/CacheKey.java vendored

@ -1,5 +1,7 @@
package apelet.common.core.cache; package apelet.common.core.cache;
import apelet.common.core.object.TokenData;
/** /**
* 在线表单缓存 * 在线表单缓存
* @author Soon * @author Soon
@ -19,6 +21,15 @@ public class CacheKey {
*/ */
public final static String FORM_FIELD_BY_HEAD_Id = "FORM_FIELD_BY_HEAD_Id:"; public final static String FORM_FIELD_BY_HEAD_Id = "FORM_FIELD_BY_HEAD_Id:";
/**
* 租户维度片段:A 端为租户Id,B 端及无请求上下文(MQ、定时任务)统一为 base。
* 租户库的表单元数据 id 可能相同(由管理端复制而来),不加租户维度会导致缓存互相覆盖(PRD FR-4.18)。
*/
private static String tenantSegment() {
Long tenantId = TokenData.currentTenantId();
return tenantId == null ? "base" : String.valueOf(tenantId);
}
public static String makeEntityTableKey(String datasourceVariableName) { public static String makeEntityTableKey(String datasourceVariableName) {
return "EntityTable:" + datasourceVariableName; return "EntityTable:" + datasourceVariableName;
} }
@ -26,32 +37,32 @@ public class CacheKey {
public static String makeTableKey(Long id) { public static String makeTableKey(Long id) {
return "FORM_HEAD_TABLE:" + id; return "FORM_HEAD_TABLE:" + tenantSegment() + ":" + id;
} }
public static String makeFormHeadByTableId(Long id) { public static String makeFormHeadByTableId(Long id) {
return "FORM_HEAD_BY_TABLE_ID:" + id; return "FORM_HEAD_BY_TABLE_ID:" + tenantSegment() + ":" + id;
} }
public static String makeTableKey(String tableName) { public static String makeTableKey(String tableName) {
return "FORM_HEAD_TABLE_NAME:" + tableName; return "FORM_HEAD_TABLE_NAME:" + tenantSegment() + ":" + tableName;
} }
public static String makeChildFieldListKey(Long id) { public static String makeChildFieldListKey(Long id) {
return "FORM_CHILD_FIELD:" + id; return "FORM_CHILD_FIELD:" + tenantSegment() + ":" + id;
} }
public static String makeOneToManyTable(String tableName) { public static String makeOneToManyTable(String tableName) {
return "FORM_ONETOMANY_TABLE:" + tableName; return "FORM_ONETOMANY_TABLE:" + tenantSegment() + ":" + tableName;
} }
public static String getFormFieldByHeadId(long headId) { public static String getFormFieldByHeadId(long headId) {
return FORM_FIELD_BY_HEAD_Id + headId; return FORM_FIELD_BY_HEAD_Id + tenantSegment() + ":" + headId;
} }

20
common/common-core/src/main/java/apelet/common/core/exception/DataNotFoundException.java

@ -0,0 +1,20 @@
package apelet.common.core.exception;
/**
* 数据不存在或不属于当前租户。
*
* <p>用于按 id 查询/更新/删除时的归属校验(PRD FR-4.16):
* 记录不存在与记录属于其他租户统一返回 404,避免通过状态码探测数据是否存在。</p>
*
* @author apelet
*/
public class DataNotFoundException extends MyRuntimeException {
public DataNotFoundException() {
super();
}
public DataNotFoundException(String msg) {
super(msg);
}
}

21
common/common-core/src/main/java/apelet/common/core/object/TokenData.java

@ -144,6 +144,27 @@ public class TokenData {
} }
/** /**
* 当前请求的租户Id。B 端(管理端)为 null,A 端(租户端)为登录用户的租户Id。
* 无 HTTP 请求上下文(如 MQ 消费、定时任务)时同样返回 null。
*
* @return 当前租户Id,可能为 null。
*/
public static Long currentTenantId() {
TokenData tokenData = takeFromRequest();
return tokenData == null ? null : tokenData.getTenantId();
}
/**
* 当前请求的应用编码。为空表示不按应用维度过滤(PRD FR-4.14)。
*
* @return 当前应用编码,可能为 null。
*/
public static String currentAppCode() {
TokenData tokenData = takeFromRequest();
return tokenData == null ? null : tokenData.getAppCode();
}
/**
* 从Http Request对象中获取令牌对象。 * 从Http Request对象中获取令牌对象。
* *
* @return 令牌对象。 * @return 令牌对象。

Loading…
Cancel
Save