feat(fl2): 落地部门目录 / 退回重提(D14) / 授权管理后端(基线 origin/main)
build / build-push (push) Failing after 55s

在 b369527 之上增量移植 FL2.0 后端能力,不改既有线级停发/指派预览/流程图入口:

- 部门目录:FlowDepartmentController(/admin/department/{groups,options,resolve,members})
  + UserBaseDepartmentDirectory(先按会话校域解析引用,再读成员;目录不可用 fail-closed)
- 退回重提 D14:POST /oapi/flow/instance/{instanceId}/resubmit,仅原提交人 CAS 路径放行,
  OAPI 侧 submitterId 路径继续 fail-closed
- 授权管理:/api/flow/admin/usage-grant/{schoolList,summary}(校域取管理员会话,无 schId 入参)
- 来源主体:解析器派生 ub-app:{applicationInfo.id},不再读取 UserBase 的 sourcePrincipalId 字段
- 回调来源策略:SELF 身份强校验;NULL 来源不广播;复合幂等键 (event_id,reg_id)
- 测试夹具:许可/主体/开通桩按新契约修正,FlowEngineIT/FormalApplyCommandIT/
  CallbackDispatchIT/OapiFlowContractIT 共 117 例在真实 MySQL 8 通过

不含:FlSourceAuthorizationTest(其断言的两个 Repository 派生方法属另一实现线)
This commit is contained in:
chenxing
2026-10-30 10:15:12 +08:00
parent b369527fe1
commit d72128672f
52 changed files with 3208 additions and 859 deletions
@@ -7,32 +7,45 @@ import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* 调用应用主体解析(D02/D09/D11:appKey 是验签凭据,不是主体)。
*
* <p>主体=UserBase {@code application_info} 主键 id(稳定整型主键,appKey 可重置/复用不影响它)。
* 加 {@link #SOURCE_PREFIX} 前缀落库,与未来可能引入的 UUID 主体空间不撞。
* 不可达、查无、缺主键一律拒绝,不回退 appKey/数字自增猜测。</p>
*/
@Service
@RequiredArgsConstructor
public class FlowSourcePrincipalResolver {
/** 来源主体列(varchar(36))的命名空间前缀:标明"id 来自 UserBase 应用注册表主键"。 */
public static final String SOURCE_PREFIX = "ub-app:";
private final OpenApiAppClient appClient;
/** 稳定主体字符串;appId 缺失即拒(调用方不得用猜测值补全)。 */
public static String sourcePrincipalIdOf(Integer appId) {
if (appId == null || appId <= 0) {
throw new BizException(ErrorCode.UNAUTHORIZED, "调用应用主体不可解析");
}
return SOURCE_PREFIX + appId;
}
public Principal resolve(String authenticatedAppKey) {
if (authenticatedAppKey == null || authenticatedAppKey.trim().isEmpty()) {
throw new BizException(ErrorCode.UNAUTHORIZED, "缺少已认证的调用应用");
}
JsonNode app;
try { app = appClient.getAppByKey(authenticatedAppKey.trim()); }
catch (RuntimeException unavailable) {
throw new BizException(ErrorCode.UNAUTHORIZED, "调用应用主体查询不可用");
try {
app = appClient.getAppByKey(authenticatedAppKey.trim());
} catch (RuntimeException unavailable) {
throw new BizException(ErrorCode.UNAUTHORIZED, "调用应用主体解析不可用,请稍后重试");
}
if (app == null || !app.path("id").isIntegralNumber() || app.path("id").intValue() <= 0) {
if (app == null || app.isNull() || !app.path("id").isIntegralNumber() || !app.path("id").canConvertToInt()) {
throw new BizException(ErrorCode.UNAUTHORIZED, "调用应用主体不可解析");
}
String source = app.path("sourcePrincipalId").asText("");
try {
if (!UUID.fromString(source).toString().equals(source)) throw new IllegalArgumentException();
} catch (IllegalArgumentException e) {
throw new BizException(ErrorCode.UNAUTHORIZED, "UserBase 稳定来源尚未迁移,禁止回退到数字 ID/appKey");
}
return new Principal(source, app.path("id").intValue(), authenticatedAppKey,
Integer appId = app.path("id").intValue();
return new Principal(sourcePrincipalIdOf(appId), appId, authenticatedAppKey.trim(),
app.path("appCode").isTextual() ? app.path("appCode").textValue() : null);
}