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):学校×流程线×稳定主体的授予/撤销/查询。
*
*
授予时把 appKey 解析成 UserBase 稳定主键 appId 落库(授权对象是主体不是凭据),
* 并按学校开通事实做发放闸(enforce 开启时未开通拒发)。撤销只置 {@code enabled=0}
* (不删行,审计与幂等判据保留);撤权只拦新发起,不回写在途实例与已受理命令。
*/
@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 listByFlow(Long schId, String flowGuid) {
return grantRepository.findBySchIdAndFlowGuidOrderByUpdateTimeDescIdDesc(schId, flowGuid);
}
public List 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;
}
}