release: FL 2.0 候选(C01 记账边界+P2-2 认领协议+D10 变量契约+D11 资格终审+D09 激活代次+T01/T03/T04/T05)
按《FL最终代码审计与上线阻断清单》194 项已审计工作树形成候选,基座 = origin/main 11e0778。
- 源码/测试/资源:沿用已审计工作树内容(96 项相对远端为新增或更新),含未跟踪实现
CallbackFactStore、CallbackSourcePolicy、VariableContract、FormalApplyCommandService、
FlowActorContextGate 等;C01 续验夹具修正一并入内。
- CI:保留远端 11e0778 的门禁强化(CI_SOURCE_SHA 核对、容器回收验证、空 schema 反向门禁、
main ref 才推镜像);ci/flow-test-schema.sql 按当前实体重新生成(14→20 表,补 6 新表与
6 新列,回调日志复合唯一键 uk_flw_log_event_reg),workflow 表数断言同步 14→20。
- 依据:docs/tasks/evidence/FL-final-audit-20261027(194 项哈希/版本核对)。
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.flowcenter.entity.FlwUsageGrant;
|
||||
import com.yunhai.flowcenter.repository.FlwUsageGrantRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 应用使用授权管理面(2026-10-25 T01):学校×流程线×稳定主体的授予/撤销/查询。
|
||||
*
|
||||
* <p>授予时把 appKey 解析成 UserBase 稳定主键 appId 落库(授权对象是主体不是凭据),
|
||||
* 并按学校开通事实做发放闸(enforce 开启时未开通拒发)。撤销只置 {@code enabled=0}
|
||||
* (不删行,审计与幂等判据保留);撤权只拦新发起,不回写在途实例与已受理命令。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlowUsageGrantAdminService {
|
||||
|
||||
private final FlwUsageGrantRepository grantRepository;
|
||||
private final UserBaseActivationClient activationClient;
|
||||
|
||||
@Autowired(required = false)
|
||||
private OpenApiAppClient appClient;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Value("${flow-center.formal-command.enabled:false}")
|
||||
private boolean formalEnabled;
|
||||
|
||||
public List<FlwUsageGrant> listByFlow(Long schId, String flowGuid) {
|
||||
return grantRepository.findBySchIdAndFlowGuidOrderByUpdateTimeDescIdDesc(schId, flowGuid);
|
||||
}
|
||||
|
||||
public List<FlwUsageGrant> listByApp(Long schId, Integer appId) {
|
||||
return grantRepository.findBySchIdAndAppIdOrderByUpdateTimeDescIdDesc(schId, appId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 授予/重申(三元键 upsert)。
|
||||
*
|
||||
* @throws BizException appKey 不可解析为稳定主体(fail-closed,管理面必须授到真实主体上);
|
||||
* enforce 开启且应用未在该校开通或核验不可用
|
||||
*/
|
||||
@Transactional
|
||||
public FlwUsageGrant grant(Long schId, String flowGuid, String appKey, String remark, String operator) {
|
||||
if (schId == null || schId <= 0 || flowGuid == null || flowGuid.trim().isEmpty()
|
||||
|| appKey == null || appKey.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "schId/flowGuid/appKey 必填");
|
||||
}
|
||||
String cleanKey = appKey.trim();
|
||||
JsonNode registered = fetchRegistered(cleanKey);
|
||||
Integer appId = registered == null ? null : readId(registered);
|
||||
if (appId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"appKey 未能在 UserBase 解析出稳定应用主体,拒绝授权:" + cleanKey);
|
||||
}
|
||||
if (activationClient.isEnforce()
|
||||
&& !activationClient.isAppActivatedAtSchool(appId, schId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN,
|
||||
"该应用未在学校 " + schId + " 开通,不能授予流程使用许可");
|
||||
}
|
||||
FlwUsageGrant grant = grantRepository
|
||||
.findBySchIdAndFlowGuidAndAppId(schId, flowGuid.trim(), appId)
|
||||
.orElseGet(FlwUsageGrant::new);
|
||||
boolean isNew = grant.getId() == null;
|
||||
if (isNew) {
|
||||
grant.setSchId(schId);
|
||||
grant.setFlowGuid(flowGuid.trim());
|
||||
grant.setAppId(appId);
|
||||
}
|
||||
if (formalEnabled) {
|
||||
String source = registered.path("sourcePrincipalId").asText("");
|
||||
try {
|
||||
if (!java.util.UUID.fromString(source).toString().equals(source)) throw new IllegalArgumentException();
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "UserBase 稳定来源未迁移,不能发放正式许可");
|
||||
}
|
||||
if (!activationClient.isAppActivatedStrict(appId, schId)) throw new BizException(ErrorCode.FORBIDDEN, "学校未开通");
|
||||
grant.setSourcePrincipalId(source);
|
||||
}
|
||||
grant.setAppKeySnapshot(cleanKey);
|
||||
grant.setAppNameSnapshot(registered.path("appName").asText(null));
|
||||
grant.setEnabled(1);
|
||||
grant.setRemark(remark);
|
||||
FlwUsageGrant saved = grantRepository.save(grant);
|
||||
log.info("[usage-grant] 授予 schId={} flowGuid={} appId={} appKey={} operator={} 新建={}",
|
||||
schId, flowGuid, appId, cleanKey, operator, isNew);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** 撤销(enabled=0,行保留);幂等:对已撤销行重复撤销不报错 */
|
||||
@Transactional
|
||||
public FlwUsageGrant revoke(Long schId, String flowGuid, Integer appId, String remark, String operator) {
|
||||
FlwUsageGrant grant = grantRepository
|
||||
.findBySchIdAndFlowGuidAndAppId(schId, flowGuid, appId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||||
"使用许可不存在:school=" + schId + " flow=" + flowGuid + " app=" + appId));
|
||||
grant.setEnabled(0);
|
||||
if (remark != null && !remark.trim().isEmpty()) {
|
||||
grant.setRemark(remark.trim());
|
||||
}
|
||||
FlwUsageGrant saved = grantRepository.save(grant);
|
||||
log.info("[usage-grant] 撤销 schId={} flowGuid={} appId={} operator={}(只拦新发起,在途不受影响)",
|
||||
schId, flowGuid, appId, operator);
|
||||
return saved;
|
||||
}
|
||||
|
||||
private JsonNode fetchRegistered(String appKey) {
|
||||
if (appClient == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "UserBase 应用注册通道未装配,拒绝授权");
|
||||
}
|
||||
try {
|
||||
JsonNode node = appClient.getAppByKey(appKey);
|
||||
return node == null || node.isNull() ? null : node;
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("[usage-grant] UserBase 注册解析失败 appKey={}:{}", appKey, e.toString());
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "UserBase 应用注册解析不可用,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private Integer readId(JsonNode registered) {
|
||||
JsonNode idNode = registered.path("id");
|
||||
return idNode.isNumber() ? idNode.intValue() : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user