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; } }