Files
flow-center-service/src/main/java/com/yunhai/flowcenter/service/FlowUsageGrantAdminService.java
T
chenxing 500a9a2fcb 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 项哈希/版本核对)。
2026-10-27 12:05:08 +08:00

134 lines
6.2 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}