feat: 收敛AI资源发布审批生命周期
- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import tech.easyflow.ai.entity.base.BotBase;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
@@ -19,6 +20,15 @@ public class Bot extends BotBase {
|
||||
public static final String KEY_MAX_MESSAGE_COUNT = "maxMessageCount";
|
||||
public static final String KEY_ENABLE_DEEP_THINKING = "enableDeepThinking";
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public boolean isAnonymousEnabled() {
|
||||
Map<String, Object> options = getOptions();
|
||||
if (options == null) {
|
||||
@@ -28,4 +38,28 @@ public class Bot extends BotBase {
|
||||
return o != null && (boolean) o;
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package tech.easyflow.ai.entity;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.easyagents.store.milvus.MilvusVectorStore;
|
||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
@@ -27,6 +28,15 @@ import java.util.Map;
|
||||
@Table("tb_document_collection")
|
||||
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||
public static final String TYPE_FAQ = "FAQ";
|
||||
|
||||
@@ -135,4 +145,28 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
}
|
||||
return options.get(key);
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
@@ -16,6 +17,15 @@ import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
@Table("tb_workflow")
|
||||
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new WorkflowTool(this, needEnglishName);
|
||||
}
|
||||
@@ -23,4 +33,28 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
public Tool toFunction(boolean needEnglishName, String definitionId) {
|
||||
return new WorkflowTool(this, needEnglishName, definitionId);
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public enum PublishStatus {
|
||||
DRAFT("DRAFT"),
|
||||
PUBLISH_PENDING("PUBLISH_PENDING"),
|
||||
PUBLISHED("PUBLISHED"),
|
||||
OFFLINE_PENDING("OFFLINE_PENDING"),
|
||||
OFFLINE("OFFLINE"),
|
||||
DELETE_PENDING("DELETE_PENDING");
|
||||
|
||||
private final String code;
|
||||
@@ -34,7 +36,16 @@ public enum PublishStatus {
|
||||
* @return 允许外部访问或线上运行时返回 true
|
||||
*/
|
||||
public boolean isExternallyVisible() {
|
||||
return this == PUBLISHED || this == DELETE_PENDING;
|
||||
return this == PUBLISHED || this == DELETE_PENDING || this == OFFLINE_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否允许作为 Bot 新绑定候选。
|
||||
*
|
||||
* @return 仅已发布状态返回 true
|
||||
*/
|
||||
public boolean isSelectableForBot() {
|
||||
return this == PUBLISHED;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期处理器抽象基类。
|
||||
*
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
public abstract class AbstractAiResourceLifecycleHandler<T> implements ApprovalSubjectHandler, AiResourceLifecycleHandler {
|
||||
|
||||
protected static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
protected static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
protected final ObjectMapper objectMapper;
|
||||
|
||||
protected AbstractAiResourceLifecycleHandler(ApprovalInstanceService approvalInstanceService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源,不存在时抛异常。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @return 资源
|
||||
*/
|
||||
protected abstract T requireResource(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 校验资源管理权限。
|
||||
*
|
||||
* @param resource 资源
|
||||
*/
|
||||
protected abstract void assertManagePermission(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源分类 ID。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 分类 ID
|
||||
*/
|
||||
protected abstract BigInteger getCategoryId(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源部门 ID。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 部门 ID
|
||||
*/
|
||||
protected abstract BigInteger getDeptId(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源标题。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 资源标题
|
||||
*/
|
||||
protected abstract String getTitle(T resource);
|
||||
|
||||
/**
|
||||
* 获取当前发布状态。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 发布状态
|
||||
*/
|
||||
protected abstract PublishStatus getCurrentStatus(T resource);
|
||||
|
||||
/**
|
||||
* 获取当前已发布快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 已发布快照
|
||||
*/
|
||||
protected abstract Map<String, Object> getPublishedSnapshot(T resource);
|
||||
|
||||
/**
|
||||
* 构建当前草稿快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 草稿快照
|
||||
*/
|
||||
protected abstract Map<String, Object> buildResourceSnapshot(T resource);
|
||||
|
||||
/**
|
||||
* 更新资源待审批状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param publishStatus 发布状态
|
||||
* @param currentApprovalInstanceId 审批实例 ID
|
||||
*/
|
||||
protected abstract void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId);
|
||||
|
||||
/**
|
||||
* 发布成功后写入已发布状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 已发布快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
protected abstract void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 下线成功后写入下线状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected abstract void markResourceOffline(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 删除资源。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected abstract void removeResource(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 返回资源中文名称。
|
||||
*
|
||||
* @return 资源名称
|
||||
*/
|
||||
protected abstract String resourceLabel();
|
||||
|
||||
/**
|
||||
* 返回当前动作摘要前缀。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @return 摘要前缀
|
||||
*/
|
||||
protected String resolveActionLabel(ApprovalActionType actionType) {
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> "发布";
|
||||
case OFFLINE -> "下线";
|
||||
case DELETE -> "删除";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展下线快照内容。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param snapshot 当前快照副本
|
||||
*/
|
||||
protected void enrichOfflineSnapshot(T resource, Map<String, Object> snapshot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前额外校验。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
*/
|
||||
protected void validateDelete(T resource, PublishStatus currentStatus) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 下线成功后的额外副作用。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除成功前的额外副作用。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected void beforeRemove(BigInteger resourceId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
T resource = requireResource(resourceId);
|
||||
assertManagePermission(resource);
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "存在未结束审批,请先处理完成");
|
||||
}
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
PublishStatus currentStatus = getCurrentStatus(resource);
|
||||
Map<String, Object> resourceSnapshot = resolveSubmitSnapshot(resource, approvalActionType, currentStatus);
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(getCategoryId(resource));
|
||||
request.setDeptId(getDeptId(resource));
|
||||
request.setSummary(resolveActionLabel(approvalActionType) + resourceLabel() + ":" + getTitle(resource));
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put(SNAPSHOT_KEY, resourceSnapshot);
|
||||
snapshot.put(PREVIOUS_STATUS_KEY, currentStatus.getCode());
|
||||
request.setSnapshotJson(snapshot);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析动作对应的提交快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param actionType 动作类型
|
||||
* @param currentStatus 当前状态
|
||||
* @return 提交快照
|
||||
*/
|
||||
protected Map<String, Object> resolveSubmitSnapshot(T resource,
|
||||
ApprovalActionType actionType,
|
||||
PublishStatus currentStatus) {
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> buildPublishSnapshot(resource, currentStatus);
|
||||
case OFFLINE -> buildOfflineSnapshot(resource, currentStatus);
|
||||
case DELETE -> buildDeleteSnapshot(resource, currentStatus);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建发布快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 发布快照
|
||||
*/
|
||||
protected Map<String, Object> buildPublishSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus != PublishStatus.DRAFT
|
||||
&& currentStatus != PublishStatus.OFFLINE
|
||||
&& currentStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "状态不允许发布");
|
||||
}
|
||||
Map<String, Object> snapshot = buildResourceSnapshot(resource);
|
||||
if (currentStatus == PublishStatus.PUBLISHED && isSameSnapshot(snapshot, getPublishedSnapshot(resource))) {
|
||||
throw new BusinessException("当前内容与已发布版本一致,无需重新发布");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建下线快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 下线快照
|
||||
*/
|
||||
protected Map<String, Object> buildOfflineSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "尚未发布,无法下线");
|
||||
}
|
||||
Map<String, Object> publishedSnapshot = getPublishedSnapshot(resource);
|
||||
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "缺少已发布快照,无法下线");
|
||||
}
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>(publishedSnapshot);
|
||||
enrichOfflineSnapshot(resource, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建删除快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 删除快照
|
||||
*/
|
||||
protected Map<String, Object> buildDeleteSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus == PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "已发布,请先下线后再删除");
|
||||
}
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "存在进行中的审批,请先处理完成");
|
||||
}
|
||||
validateDelete(resource, currentStatus);
|
||||
return buildResourceSnapshot(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行审批通过后的真实动作。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
@Override
|
||||
public void applyApprovedAction(String actionType,
|
||||
BigInteger resourceId,
|
||||
Map<String, Object> resourceSnapshot,
|
||||
BigInteger operatorId) {
|
||||
ApprovalActionType normalizedAction = ApprovalActionType.from(actionType);
|
||||
if (normalizedAction == ApprovalActionType.PUBLISH) {
|
||||
publishResource(resourceId, resourceSnapshot, operatorId);
|
||||
return;
|
||||
}
|
||||
if (normalizedAction == ApprovalActionType.OFFLINE) {
|
||||
markResourceOffline(resourceId);
|
||||
afterOffline(resourceId);
|
||||
return;
|
||||
}
|
||||
beforeRemove(resourceId);
|
||||
removeResource(resourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId) {
|
||||
persistResourceState(resourceId, publishStatus, instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void restoreState(BigInteger resourceId, PublishStatus previousStatus) {
|
||||
persistResourceState(resourceId, previousStatus, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求快照读取资源内容。
|
||||
*
|
||||
* @param snapshotJson 请求快照
|
||||
* @return 资源快照
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少" + resourceLabel() + "内容");
|
||||
}
|
||||
return new LinkedHashMap<>((Map<String, Object>) map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前草稿快照与已发布快照是否一致。
|
||||
*
|
||||
* @param currentSnapshot 当前快照
|
||||
* @param publishedSnapshot 已发布快照
|
||||
* @return 一致返回 true
|
||||
*/
|
||||
protected boolean isSameSnapshot(Map<String, Object> currentSnapshot, Map<String, Object> publishedSnapshot) {
|
||||
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(
|
||||
objectMapper.valueToTree(currentSnapshot),
|
||||
objectMapper.valueToTree(publishedSnapshot)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期处理器。
|
||||
*/
|
||||
public interface AiResourceLifecycleHandler {
|
||||
|
||||
/**
|
||||
* 当前处理器支持的资源类型。
|
||||
*
|
||||
* @return 资源类型编码
|
||||
*/
|
||||
String resourceType();
|
||||
|
||||
/**
|
||||
* 构建动作提交请求。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 提交请求
|
||||
*/
|
||||
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 写入待审批状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param publishStatus 待写入的发布状态
|
||||
* @param instanceId 审批实例 ID
|
||||
*/
|
||||
void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId);
|
||||
|
||||
/**
|
||||
* 执行动作真正生效后的资源更新与副作用。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 审批冻结快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
void applyApprovedAction(String actionType, BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 按提交前真实状态恢复资源状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param previousStatus 提交前状态
|
||||
*/
|
||||
void restoreState(BigInteger resourceId, PublishStatus previousStatus);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期统一状态机服务。
|
||||
*/
|
||||
public interface AiResourceLifecycleService {
|
||||
|
||||
/**
|
||||
* 提交资源动作。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 执行结果
|
||||
*/
|
||||
ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期统一状态机实现。
|
||||
*/
|
||||
@Service
|
||||
public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleService, ApprovalResultHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
private static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||
|
||||
private final List<AiResourceLifecycleHandler> handlers;
|
||||
private final ApprovalMatchService approvalMatchService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
|
||||
public AiResourceLifecycleServiceImpl(List<AiResourceLifecycleHandler> handlers,
|
||||
ApprovalMatchService approvalMatchService,
|
||||
ApprovalInstanceService approvalInstanceService) {
|
||||
this.handlers = handlers;
|
||||
this.approvalMatchService = approvalMatchService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
AiResourceLifecycleHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||
if (flow == null) {
|
||||
handler.applyApprovedAction(actionType, resourceId, readResourceSnapshot(request.getSnapshotJson()), operatorId);
|
||||
return ApprovalActionResult.direct();
|
||||
}
|
||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||
handler.updatePendingState(
|
||||
resourceId,
|
||||
resolveSubmittedStatus(actionType, resolvePreviousStatus(request.getSnapshotJson())),
|
||||
instanceId
|
||||
);
|
||||
return ApprovalActionResult.required(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).applyApprovedAction(
|
||||
instance.getActionType(),
|
||||
instance.getResourceId(),
|
||||
readResourceSnapshot(instance.getSnapshotJson()),
|
||||
operatorId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).restoreState(
|
||||
instance.getResourceId(),
|
||||
resolvePreviousStatus(instance.getSnapshotJson())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).restoreState(
|
||||
instance.getResourceId(),
|
||||
resolvePreviousStatus(instance.getSnapshotJson())
|
||||
);
|
||||
}
|
||||
|
||||
private AiResourceLifecycleHandler getHandler(String resourceType) {
|
||||
return handlers.stream()
|
||||
.filter(item -> item.resourceType().equals(resourceType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("未找到资源生命周期处理器: " + resourceType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析提交动作后的真实持久化状态。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param previousStatus 提交前真实状态
|
||||
* @return 提交后应写入的状态
|
||||
*/
|
||||
private PublishStatus resolveSubmittedStatus(String actionType, PublishStatus previousStatus) {
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(actionType) && previousStatus == PublishStatus.PUBLISHED) {
|
||||
return PublishStatus.PUBLISHED;
|
||||
}
|
||||
if (ApprovalActionType.DELETE.getCode().equals(actionType)) {
|
||||
return PublishStatus.DELETE_PENDING;
|
||||
}
|
||||
if (ApprovalActionType.OFFLINE.getCode().equals(actionType)) {
|
||||
return PublishStatus.OFFLINE_PENDING;
|
||||
}
|
||||
return PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从审批快照解析提交前真实状态。
|
||||
*
|
||||
* @param snapshotJson 审批冻结快照
|
||||
* @return 提交前真实状态
|
||||
*/
|
||||
private PublishStatus resolvePreviousStatus(Map<String, Object> snapshotJson) {
|
||||
Object status = snapshotJson == null ? null : snapshotJson.get(PREVIOUS_STATUS_KEY);
|
||||
if (status instanceof String value && !value.isBlank()) {
|
||||
return PublishStatus.from(value);
|
||||
}
|
||||
return PublishStatus.DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从审批快照中提取冻结资源内容。
|
||||
*
|
||||
* @param snapshotJson 审批冻结快照
|
||||
* @return 冻结资源内容
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少资源内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotCategory;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
@@ -26,35 +26,27 @@ import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 聊天助手审批处理器。
|
||||
* 聊天助手生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class BotApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Bot> {
|
||||
|
||||
private final BotService botService;
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
@@ -63,7 +55,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private final BotMcpService botMcpService;
|
||||
private final WorkflowService workflowService;
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ModelService modelService;
|
||||
private final BotCategoryService botCategoryService;
|
||||
@@ -84,7 +75,9 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
BotCategoryService botCategoryService,
|
||||
SysDeptService sysDeptService,
|
||||
PluginItemService pluginItemService,
|
||||
McpService mcpService) {
|
||||
McpService mcpService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.botService = botService;
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
@@ -92,7 +85,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
this.botMcpService = botMcpService;
|
||||
this.workflowService = workflowService;
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.modelService = modelService;
|
||||
this.botCategoryService = botCategoryService;
|
||||
@@ -106,69 +98,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.BOT.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
Bot bot = requireBot(resourceId);
|
||||
assertManagePermission(bot);
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前聊天助手存在未结束审批,请先处理完成");
|
||||
}
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(bot);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(bot.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前聊天助手没有变更,无需重复发布");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(bot.getCategoryId());
|
||||
request.setDeptId(bot.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "聊天助手:" + bot.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
Bot update = new Bot();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
Bot update = new Bot();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
botService.updateById(update);
|
||||
return;
|
||||
}
|
||||
removeBotRelations(instance.getResourceId());
|
||||
botService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Bot bot = botService.getDetail(String.valueOf(identifier));
|
||||
@@ -178,24 +107,52 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private Bot requireBot(BigInteger id) {
|
||||
Bot bot = botService.getById(id);
|
||||
@Override
|
||||
protected Bot requireResource(BigInteger resourceId) {
|
||||
Bot bot = botService.getById(resourceId);
|
||||
if (bot == null) {
|
||||
throw new BusinessException("聊天助手不存在");
|
||||
}
|
||||
return bot;
|
||||
}
|
||||
|
||||
private void assertManagePermission(Bot bot) {
|
||||
@Override
|
||||
protected void assertManagePermission(Bot resource) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin();
|
||||
boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy());
|
||||
boolean creator = account != null && account.getId() != null && account.getId().equals(resource.getCreatedBy());
|
||||
if (!superAdmin && !creator) {
|
||||
throw new BusinessException("仅创建者或超级管理员可管理聊天助手");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(Bot bot) {
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Bot resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Bot resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Bot resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Bot resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Bot resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Bot bot) {
|
||||
Model model = resolveModel(bot.getModelId());
|
||||
BotCategory category = resolveCategory(bot.getCategoryId());
|
||||
SysDept dept = resolveDept(bot.getDeptId());
|
||||
@@ -228,13 +185,59 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
botService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "聊天助手";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRemove(BigInteger resourceId) {
|
||||
removeBotRelations(resourceId);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildWorkflowBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId);
|
||||
List<BotWorkflow> relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotWorkflow::getWorkflowId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotWorkflow relation : relations) {
|
||||
Workflow workflow = relation.getWorkflow();
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isSelectableForBot()) {
|
||||
throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
@@ -248,10 +251,11 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildKnowledgeBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId);
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotDocumentCollection::getDocumentCollectionId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotDocumentCollection relation : relations) {
|
||||
DocumentCollection knowledge = relation.getKnowledge();
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isSelectableForBot()) {
|
||||
throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
@@ -266,6 +270,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildPluginBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId);
|
||||
List<BotPlugin> relations = botPluginService.list(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotPlugin::getPluginItemId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotPlugin relation : relations) {
|
||||
PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId());
|
||||
@@ -274,7 +279,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("pluginItemId", relation.getPluginItemId());
|
||||
item.put("pluginItemName", resolvePluginName(pluginItem));
|
||||
item.put("pluginItemName", pluginItem.getName());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
@@ -283,156 +288,85 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildMcpBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId);
|
||||
List<BotMcp> relations = botMcpService.list(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotMcp::getMcpId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotMcp relation : relations) {
|
||||
Mcp mcp = mcpService.getById(relation.getMcpId());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("聊天助手绑定的MCP不存在,无法发布聊天助手");
|
||||
throw new BusinessException("聊天助手绑定的 MCP 不存在,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("mcpId", relation.getMcpId());
|
||||
item.put("mcpName", mcp.getTitle());
|
||||
item.put("mcpToolName", relation.getMcpToolName());
|
||||
item.put("mcpToolDescription", relation.getMcpToolDescription());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天助手分类。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 分类实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private BotCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
}
|
||||
return botCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门信息。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 部门实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
}
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @return 模型实体
|
||||
*/
|
||||
private Model resolveModel(BigInteger modelId) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
||||
}
|
||||
Model model = modelService.getById(modelId);
|
||||
if (model == null) {
|
||||
throw new BusinessException("聊天助手绑定的模型不存在,无法提交审批");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模型展示名称。
|
||||
*
|
||||
* @param model 模型实体
|
||||
* @return 模型名称
|
||||
*/
|
||||
private String resolveModelName(Model model) {
|
||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
||||
return model.getTitle();
|
||||
}
|
||||
return model.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取系统提示词。
|
||||
*
|
||||
* @param bot 聊天助手
|
||||
* @return 系统提示词
|
||||
*/
|
||||
private String resolveSystemPrompt(Bot bot) {
|
||||
if (bot.getModelOptions() == null) {
|
||||
return null;
|
||||
}
|
||||
Object prompt = bot.getModelOptions().get(Bot.KEY_SYSTEM_PROMPT);
|
||||
return prompt == null ? null : String.valueOf(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数值配置项。
|
||||
*
|
||||
* @param options 配置 map
|
||||
* @param key 配置键
|
||||
* @return 数值配置,不存在时返回 {@code null}
|
||||
*/
|
||||
private Number readNumberOption(Map<String, Object> options, String key) {
|
||||
if (options == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = options.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成插件工具展示名称。
|
||||
*
|
||||
* @param pluginItem 插件工具实体
|
||||
* @return 展示名称
|
||||
*/
|
||||
private String resolvePluginName(PluginItem pluginItem) {
|
||||
if (pluginItem.getName() != null && !pluginItem.getName().isBlank()) {
|
||||
return pluginItem.getName();
|
||||
}
|
||||
return pluginItem.getEnglishName();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少聊天助手发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger botId) {
|
||||
Bot bot = botService.getById(botId);
|
||||
if (bot == null) {
|
||||
return;
|
||||
}
|
||||
Bot update = new Bot();
|
||||
update.setId(botId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
private void removeBotRelations(BigInteger botId) {
|
||||
botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
|
||||
botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
|
||||
botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
|
||||
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
|
||||
}
|
||||
|
||||
private BotCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
}
|
||||
return botCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
}
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
private Model resolveModel(BigInteger modelId) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
||||
}
|
||||
Model model = modelService.getById(modelId);
|
||||
if (model == null) {
|
||||
throw new BusinessException("聊天助手关联模型不存在,无法提交审批");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private String resolveModelName(Model model) {
|
||||
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||
if (providerName == null || providerName.isBlank()) {
|
||||
return model.getModelName();
|
||||
}
|
||||
return providerName + " / " + model.getModelName();
|
||||
}
|
||||
|
||||
private String resolveSystemPrompt(Bot bot) {
|
||||
if (bot.getModelOptions() == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = bot.getModelOptions().get("systemPrompt");
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Number readNumberOption(Map<String, Object> options, String key) {
|
||||
if (options == null || !options.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
Object value = options.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number;
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Double.valueOf(string);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,36 +15,58 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class BotPublishAppService {
|
||||
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public BotPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手发布审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "BOT",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手下线审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手删除审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "BOT",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
@@ -12,43 +12,36 @@ import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库审批处理器。
|
||||
* 知识库生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<DocumentCollection> {
|
||||
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||
private final ModelService modelService;
|
||||
private final DocumentCollectionCategoryService documentCollectionCategoryService;
|
||||
private final SysDeptService sysDeptService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
|
||||
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
@@ -56,14 +49,17 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
BotDocumentCollectionService botDocumentCollectionService,
|
||||
ModelService modelService,
|
||||
DocumentCollectionCategoryService documentCollectionCategoryService,
|
||||
SysDeptService sysDeptService) {
|
||||
SysDeptService sysDeptService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
this.modelService = modelService;
|
||||
this.documentCollectionCategoryService = documentCollectionCategoryService;
|
||||
this.sysDeptService = sysDeptService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,72 +67,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.KNOWLEDGE.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
DocumentCollection knowledge = requireKnowledge(resourceId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前知识库存在未结束审批,请先处理完成");
|
||||
}
|
||||
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(knowledge);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(knowledge.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前知识库没有变更,无需重复发布");
|
||||
}
|
||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(knowledge.getCategoryId());
|
||||
request.setDeptId(knowledge.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "知识库:" + knowledge.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
documentCollectionService.updateById(update);
|
||||
return;
|
||||
}
|
||||
documentCollectionService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
||||
@@ -146,20 +76,47 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentCollection requireKnowledge(BigInteger id) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(id);
|
||||
@Override
|
||||
protected DocumentCollection requireResource(BigInteger resourceId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(resourceId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger knowledgeId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
||||
return botDocumentCollectionService.exists(queryWrapper);
|
||||
@Override
|
||||
protected void assertManagePermission(DocumentCollection resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, resource, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
||||
@Override
|
||||
protected BigInteger getCategoryId(DocumentCollection resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(DocumentCollection resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(DocumentCollection resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(DocumentCollection resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(DocumentCollection resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
||||
Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批");
|
||||
Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批");
|
||||
DocumentCollectionCategory category = resolveCategory(collection.getCategoryId());
|
||||
@@ -197,12 +154,82 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库分类信息。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 分类实体,不存在时返回 {@code null}
|
||||
*/
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
documentCollectionService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "知识库";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enrichOfflineSnapshot(DocumentCollection resource, Map<String, Object> snapshot) {
|
||||
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkKnowledgeImpact(resource.getId());
|
||||
if (!impact.isCanProceed()) {
|
||||
throw new BusinessException(buildWorkflowUsageBlockMessage(impact));
|
||||
}
|
||||
if (impact.isHasBotBindings()) {
|
||||
snapshot.put("botBindings", impact.getBotBindings());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateDelete(DocumentCollection resource, PublishStatus currentStatus) {
|
||||
if (hasBotBinding(resource.getId())) {
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
resourceOfflineImpactService.unbindKnowledgeFromBots(resourceId);
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger knowledgeId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
||||
return botDocumentCollectionService.exists(queryWrapper);
|
||||
}
|
||||
|
||||
private String buildWorkflowUsageBlockMessage(OfflineImpactCheckVo impact) {
|
||||
String names = impact.getWorkflowUsages().stream()
|
||||
.map(item -> item.getTitle() == null ? String.valueOf(item.getId()) : item.getTitle())
|
||||
.reduce((left, right) -> left + "、" + right)
|
||||
.orElse("未知工作流");
|
||||
return "当前知识库被以下工作流使用:" + names + ",请先在工作流中调整解绑后再下线";
|
||||
}
|
||||
|
||||
private DocumentCollectionCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
@@ -210,12 +237,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return documentCollectionCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门信息。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 部门实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
@@ -223,13 +244,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析必填模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @param errorMessage 模型不存在时抛出的提示
|
||||
* @return 模型实体
|
||||
*/
|
||||
private Model resolveModel(BigInteger modelId, String errorMessage) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException(errorMessage);
|
||||
@@ -241,13 +255,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析可选模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @param errorMessage 模型不存在时抛出的提示
|
||||
* @return 模型实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private Model resolveOptionalModel(BigInteger modelId, String errorMessage) {
|
||||
if (modelId == null) {
|
||||
return null;
|
||||
@@ -259,75 +266,30 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模型展示名称。
|
||||
*
|
||||
* @param model 模型实体
|
||||
* @return 模型名称
|
||||
*/
|
||||
private String resolveModelName(Model model) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
||||
return model.getTitle();
|
||||
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||
if (providerName == null || providerName.isBlank()) {
|
||||
return model.getModelName();
|
||||
}
|
||||
return model.getModelName();
|
||||
return providerName + " / " + model.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库可见范围文案。
|
||||
*
|
||||
* @param visibilityScope 可见范围编码
|
||||
* @return 展示文案
|
||||
*/
|
||||
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
||||
return switch (VisibilityScope.fromOrDefault(visibilityScope, VisibilityScope.PRIVATE)) {
|
||||
case DEPT -> "部门";
|
||||
case PUBLIC -> "公开";
|
||||
default -> "个人";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库类型文案。
|
||||
*
|
||||
* @param collectionType 知识库类型
|
||||
* @return 展示文案
|
||||
*/
|
||||
private String resolveCollectionTypeLabel(String collectionType) {
|
||||
if (DocumentCollection.TYPE_FAQ.equalsIgnoreCase(collectionType)) {
|
||||
if ("FAQ".equalsIgnoreCase(collectionType)) {
|
||||
return "FAQ";
|
||||
}
|
||||
return "文档";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少知识库发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger knowledgeId) {
|
||||
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(knowledgeId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
documentCollectionService.updateById(update);
|
||||
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
||||
VisibilityScope scope = VisibilityScope.from(visibilityScope);
|
||||
return switch (scope) {
|
||||
case PRIVATE -> "仅自己";
|
||||
case DEPT -> "本部门";
|
||||
case PUBLIC -> "公开";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,36 +20,82 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class KnowledgePublishAppService {
|
||||
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public KnowledgePublishAppService(DocumentCollectionService documentCollectionService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库发布审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "KNOWLEDGE",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库下线审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查知识库下线影响。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||
assertId(id);
|
||||
DocumentCollection knowledge = documentCollectionService.getById(id);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前知识库尚未发布,无法下线");
|
||||
}
|
||||
return resourceOfflineImpactService.checkKnowledgeImpact(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库删除审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "KNOWLEDGE",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id) {
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流审批处理器。
|
||||
* 工作流生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Workflow> {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
|
||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
ApprovalInstanceService approvalInstanceService,
|
||||
BotWorkflowService botWorkflowService) {
|
||||
BotWorkflowService botWorkflowService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,72 +50,6 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.WORKFLOW.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
Workflow workflow = requireWorkflow(resourceId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前工作流存在未结束审批,请先处理完成");
|
||||
}
|
||||
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(workflow);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(workflow.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前工作流没有变更,无需重复发布");
|
||||
}
|
||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
||||
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(workflow.getCategoryId());
|
||||
request.setDeptId(workflow.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "工作流:" + workflow.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
workflowService.updateById(update);
|
||||
return;
|
||||
}
|
||||
workflowService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
||||
@@ -129,62 +59,125 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private Workflow requireWorkflow(BigInteger id) {
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
@Override
|
||||
protected Workflow requireResource(BigInteger resourceId) {
|
||||
Workflow workflow = workflowService.getById(resourceId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertManagePermission(Workflow resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, resource, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Workflow resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Workflow resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Workflow resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Workflow resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Workflow resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Workflow resource) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", resource.getId());
|
||||
snapshot.put("alias", resource.getAlias());
|
||||
snapshot.put("deptId", resource.getDeptId());
|
||||
snapshot.put("tenantId", resource.getTenantId());
|
||||
snapshot.put("title", resource.getTitle());
|
||||
snapshot.put("description", resource.getDescription());
|
||||
snapshot.put("icon", resource.getIcon());
|
||||
snapshot.put("content", resource.getContent());
|
||||
snapshot.put("englishName", resource.getEnglishName());
|
||||
snapshot.put("status", resource.getStatus());
|
||||
snapshot.put("categoryId", resource.getCategoryId());
|
||||
snapshot.put("visibilityScope", resource.getVisibilityScope());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
workflowService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "工作流";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enrichOfflineSnapshot(Workflow resource, Map<String, Object> snapshot) {
|
||||
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
|
||||
if (impact.isHasBotBindings()) {
|
||||
snapshot.put("botBindings", impact.getBotBindings());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateDelete(Workflow resource, PublishStatus currentStatus) {
|
||||
if (hasBotBinding(resource.getId())) {
|
||||
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
resourceOfflineImpactService.unbindWorkflowFromBots(resourceId);
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger workflowId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
|
||||
return botWorkflowService.exists(queryWrapper);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(Workflow workflow) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", workflow.getId());
|
||||
snapshot.put("alias", workflow.getAlias());
|
||||
snapshot.put("deptId", workflow.getDeptId());
|
||||
snapshot.put("tenantId", workflow.getTenantId());
|
||||
snapshot.put("title", workflow.getTitle());
|
||||
snapshot.put("description", workflow.getDescription());
|
||||
snapshot.put("icon", workflow.getIcon());
|
||||
snapshot.put("content", workflow.getContent());
|
||||
snapshot.put("englishName", workflow.getEnglishName());
|
||||
snapshot.put("status", workflow.getStatus());
|
||||
snapshot.put("categoryId", workflow.getCategoryId());
|
||||
snapshot.put("visibilityScope", workflow.getVisibilityScope());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少工作流发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger workflowId) {
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
return;
|
||||
}
|
||||
Workflow update = new Workflow();
|
||||
update.setId(workflowId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -14,36 +20,82 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class WorkflowPublishAppService {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public WorkflowPublishAppService(WorkflowService workflowService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.workflowService = workflowService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流发布审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "WORKFLOW",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流下线审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工作流下线影响。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
if (PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前工作流尚未发布,无法下线");
|
||||
}
|
||||
return resourceOfflineImpactService.checkWorkflowImpact(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流删除审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "WORKFLOW",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode());
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id, String resourceType, String actionType) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* AI 资源审批状态派生服务。
|
||||
* <p>
|
||||
* 该服务仅负责根据当前审批实例与资源真实状态派生只读展示字段,
|
||||
* 不参与资源状态回写,也不在读链路中执行任何数据库修复操作。
|
||||
*/
|
||||
public interface AiResourceApprovalStateService {
|
||||
|
||||
/**
|
||||
* 填充工作流审批展示状态。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
*/
|
||||
void fillWorkflowApprovalState(Workflow workflow);
|
||||
|
||||
/**
|
||||
* 批量填充工作流审批展示状态。
|
||||
*
|
||||
* @param workflows 工作流集合
|
||||
*/
|
||||
void fillWorkflowApprovalState(Collection<Workflow> workflows);
|
||||
|
||||
/**
|
||||
* 填充知识库审批展示状态。
|
||||
*
|
||||
* @param collection 知识库
|
||||
*/
|
||||
void fillKnowledgeApprovalState(DocumentCollection collection);
|
||||
|
||||
/**
|
||||
* 批量填充知识库审批展示状态。
|
||||
*
|
||||
* @param collections 知识库集合
|
||||
*/
|
||||
void fillKnowledgeApprovalState(Collection<DocumentCollection> collections);
|
||||
|
||||
/**
|
||||
* 填充聊天助手审批展示状态。
|
||||
*
|
||||
* @param bot 聊天助手
|
||||
*/
|
||||
void fillBotApprovalState(Bot bot);
|
||||
|
||||
/**
|
||||
* 批量填充聊天助手审批展示状态。
|
||||
*
|
||||
* @param bots 聊天助手集合
|
||||
*/
|
||||
void fillBotApprovalState(Collection<Bot> bots);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 资源下线影响检查与解绑服务。
|
||||
*/
|
||||
public interface ResourceOfflineImpactService {
|
||||
|
||||
/**
|
||||
* 检查工作流下线影响。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId);
|
||||
|
||||
/**
|
||||
* 检查知识库下线影响。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId);
|
||||
|
||||
/**
|
||||
* 工作流下线后,静默解绑所有关联 Bot。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
*/
|
||||
void unbindWorkflowFromBots(BigInteger workflowId);
|
||||
|
||||
/**
|
||||
* 知识库下线后,静默解绑所有关联 Bot。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
*/
|
||||
void unbindKnowledgeFromBots(BigInteger knowledgeId);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI 资源审批状态派生服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalStateService {
|
||||
|
||||
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
public AiResourceApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillWorkflowApprovalState(Workflow workflow) {
|
||||
fillWorkflowApprovalState(workflow == null ? List.of() : List.of(workflow));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillWorkflowApprovalState(Collection<Workflow> workflows) {
|
||||
fillApprovalState(
|
||||
workflows,
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
Workflow::getCurrentApprovalInstanceId,
|
||||
workflow -> PublishStatus.from(workflow.getPublishStatus()),
|
||||
Workflow::getPublishedSnapshotJson,
|
||||
Workflow::setApprovalPending,
|
||||
Workflow::setCurrentApprovalActionType,
|
||||
Workflow::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillKnowledgeApprovalState(DocumentCollection collection) {
|
||||
fillKnowledgeApprovalState(collection == null ? List.of() : List.of(collection));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillKnowledgeApprovalState(Collection<DocumentCollection> collections) {
|
||||
fillApprovalState(
|
||||
collections,
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
DocumentCollection::getCurrentApprovalInstanceId,
|
||||
collection -> PublishStatus.from(collection.getPublishStatus()),
|
||||
DocumentCollection::getPublishedSnapshotJson,
|
||||
DocumentCollection::setApprovalPending,
|
||||
DocumentCollection::setCurrentApprovalActionType,
|
||||
DocumentCollection::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillBotApprovalState(Bot bot) {
|
||||
fillBotApprovalState(bot == null ? List.of() : List.of(bot));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillBotApprovalState(Collection<Bot> bots) {
|
||||
fillApprovalState(
|
||||
bots,
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
Bot::getCurrentApprovalInstanceId,
|
||||
bot -> PublishStatus.from(bot.getPublishStatus()),
|
||||
Bot::getPublishedSnapshotJson,
|
||||
Bot::setApprovalPending,
|
||||
Bot::setCurrentApprovalActionType,
|
||||
Bot::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一派生审批展示状态。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param resourceType 资源类型
|
||||
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||
* @param statusGetter 发布状态获取器
|
||||
* @param snapshotGetter 已发布快照获取器
|
||||
* @param pendingSetter 审批中标记写入器
|
||||
* @param actionSetter 当前审批动作写入器
|
||||
* @param displaySetter 展示状态写入器
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
private <T> void fillApprovalState(Collection<T> resources,
|
||||
String resourceType,
|
||||
Function<T, BigInteger> instanceIdGetter,
|
||||
Function<T, PublishStatus> statusGetter,
|
||||
Function<T, Map<String, Object>> snapshotGetter,
|
||||
BiConsumer<T, Boolean> pendingSetter,
|
||||
BiConsumer<T, String> actionSetter,
|
||||
BiConsumer<T, String> displaySetter) {
|
||||
if (CollectionUtils.isEmpty(resources)) {
|
||||
return;
|
||||
}
|
||||
List<T> validResources = resources.stream().filter(Objects::nonNull).toList();
|
||||
if (validResources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validResources, instanceIdGetter);
|
||||
for (T resource : validResources) {
|
||||
PublishStatus currentStatus = statusGetter.apply(resource);
|
||||
BigInteger instanceId = instanceIdGetter.apply(resource);
|
||||
ApprovalInstance instance = instanceId == null ? null : instanceMap.get(instanceId);
|
||||
if (!isValidCurrentInstance(resourceType, instance)) {
|
||||
pendingSetter.accept(resource, false);
|
||||
actionSetter.accept(resource, null);
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||
if (instanceStatus.isFinished()) {
|
||||
pendingSetter.accept(resource, false);
|
||||
actionSetter.accept(resource, null);
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||
pendingSetter.accept(resource, true);
|
||||
actionSetter.accept(resource, actionType.getCode());
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载当前审批实例。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||
* @param <T> 资源类型
|
||||
* @return 审批实例映射
|
||||
*/
|
||||
private <T> Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<T> resources,
|
||||
Function<T, BigInteger> instanceIdGetter) {
|
||||
Set<BigInteger> instanceIds = resources.stream()
|
||||
.map(instanceIdGetter)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (instanceIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||
);
|
||||
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断审批实例是否仍是当前资源的有效实例。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param instance 审批实例
|
||||
* @return 有效返回 true
|
||||
*/
|
||||
private boolean isValidCurrentInstance(String resourceType, ApprovalInstance instance) {
|
||||
return instance != null && resourceType.equals(instance.getResourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在没有活动审批实例时派生展示状态。
|
||||
*
|
||||
* @param currentStatus 当前真实状态
|
||||
* @param publishedSnapshot 已发布快照
|
||||
* @return 展示状态
|
||||
*/
|
||||
private PublishStatus resolveDisplayStatusWithoutActiveInstance(PublishStatus currentStatus,
|
||||
Map<String, Object> publishedSnapshot) {
|
||||
// 读接口不再猜测或掩盖真实状态。若资源表已经落成 pending 但当前找不到有效审批实例,
|
||||
// 页面应直接看到真实状态,后续再由独立修复流程处理脏数据,而不是在展示层伪装成已发布。
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在存在活动审批实例时派生展示状态。
|
||||
*
|
||||
* @param currentStatus 当前真实状态
|
||||
* @param actionType 当前审批动作
|
||||
* @return 展示状态
|
||||
*/
|
||||
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||
ApprovalActionType actionType) {
|
||||
if (currentStatus == PublishStatus.PUBLISHED && actionType == ApprovalActionType.PUBLISH) {
|
||||
return PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
return currentStatus;
|
||||
}
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 资源下线影响检查与 Bot 静默解绑实现。
|
||||
*/
|
||||
@Service
|
||||
public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactService {
|
||||
|
||||
private static final String KNOWLEDGE_NODE_TYPE = "knowledgeNode";
|
||||
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
|
||||
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||
private final BotService botService;
|
||||
private final WorkflowService workflowService;
|
||||
private final RedisLockExecutor redisLockExecutor;
|
||||
|
||||
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService,
|
||||
BotService botService,
|
||||
WorkflowService workflowService,
|
||||
RedisLockExecutor redisLockExecutor) {
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
this.botService = botService;
|
||||
this.workflowService = workflowService;
|
||||
this.redisLockExecutor = redisLockExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
|
||||
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
|
||||
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||
result.setCanProceed(true);
|
||||
result.setBotBindings(botBindings);
|
||||
result.setHasBotBindings(!botBindings.isEmpty());
|
||||
result.setWorkflowUsages(Collections.emptyList());
|
||||
result.setHasWorkflowUsages(false);
|
||||
result.setMessage(botBindings.isEmpty()
|
||||
? "当前工作流下线后不会影响已有绑定"
|
||||
: "当前工作流下线成功后,将自动从相关聊天助手中解绑");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId) {
|
||||
List<OfflineImpactBindingVo> botBindings = listBotsByKnowledgeId(knowledgeId);
|
||||
List<OfflineImpactBindingVo> workflowUsages = listWorkflowsUsingKnowledge(knowledgeId);
|
||||
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||
result.setBotBindings(botBindings);
|
||||
result.setHasBotBindings(!botBindings.isEmpty());
|
||||
result.setWorkflowUsages(workflowUsages);
|
||||
result.setHasWorkflowUsages(!workflowUsages.isEmpty());
|
||||
result.setCanProceed(workflowUsages.isEmpty());
|
||||
result.setMessage(workflowUsages.isEmpty()
|
||||
? (botBindings.isEmpty()
|
||||
? "当前知识库下线后不会影响已有绑定"
|
||||
: "当前知识库下线成功后,将自动从相关聊天助手中解绑")
|
||||
: "当前知识库仍被工作流使用,请先调整工作流后再下线");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindWorkflowFromBots(BigInteger workflowId) {
|
||||
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
Set<BigInteger> botIds = collectBotIds(relations, BotWorkflow::getBotId);
|
||||
for (BigInteger botId : botIds) {
|
||||
redisLockExecutor.executeWithLock(
|
||||
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
botWorkflowService.remove(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getBotId, botId)
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
trimPublishedSnapshotBindings(botId, "workflowBindings", "workflowId", workflowId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindKnowledgeFromBots(BigInteger knowledgeId) {
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
Set<BigInteger> botIds = collectBotIds(relations, BotDocumentCollection::getBotId);
|
||||
for (BigInteger botId : botIds) {
|
||||
redisLockExecutor.executeWithLock(
|
||||
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
botDocumentCollectionService.remove(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getBotId, botId)
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
trimPublishedSnapshotBindings(botId, "knowledgeBindings", "knowledgeId", knowledgeId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByWorkflowId(BigInteger workflowId) {
|
||||
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
return listBotsByIds(collectBotIds(relations, BotWorkflow::getBotId));
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByKnowledgeId(BigInteger knowledgeId) {
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
return listBotsByIds(collectBotIds(relations, BotDocumentCollection::getBotId));
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByIds(Set<BigInteger> botIds) {
|
||||
if (botIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Bot> bots = botService.listByIds(botIds);
|
||||
Map<BigInteger, Bot> botMap = new HashMap<>();
|
||||
for (Bot bot : bots) {
|
||||
botMap.put(bot.getId(), bot);
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>(botIds.size());
|
||||
for (BigInteger botId : botIds) {
|
||||
Bot bot = botMap.get(botId);
|
||||
if (bot == null) {
|
||||
continue;
|
||||
}
|
||||
result.add(toBindingVo(bot.getId(), bot.getTitle()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listWorkflowsUsingKnowledge(BigInteger knowledgeId) {
|
||||
List<Workflow> workflows = workflowService.list();
|
||||
if (workflows == null || workflows.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>();
|
||||
for (Workflow workflow : workflows) {
|
||||
if (workflow == null || workflow.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (containsKnowledgeReference(workflow.getContent(), knowledgeId)) {
|
||||
result.add(toBindingVo(workflow.getId(), workflow.getTitle()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
|
||||
if (!StringUtils.hasText(content) || knowledgeId == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JSON.parse(content);
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
return false;
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String expected = knowledgeId.toString();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null || !KNOWLEDGE_NODE_TYPE.equals(node.getString("type"))) {
|
||||
continue;
|
||||
}
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
Object rawKnowledgeId = data.get("knowledgeId");
|
||||
if (rawKnowledgeId != null && expected.equals(String.valueOf(rawKnowledgeId))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void trimPublishedSnapshotBindings(BigInteger botId,
|
||||
String bindingsKey,
|
||||
String idKey,
|
||||
BigInteger resourceId) {
|
||||
Bot bot = botService.getById(botId);
|
||||
if (bot == null || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>(bot.getPublishedSnapshotJson());
|
||||
Object rawBindings = snapshot.get(bindingsKey);
|
||||
if (!(rawBindings instanceof List<?> bindings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> filtered = new ArrayList<>();
|
||||
boolean changed = false;
|
||||
String expectedId = resourceId == null ? null : resourceId.toString();
|
||||
for (Object item : bindings) {
|
||||
if (!(item instanceof Map<?, ?> bindingMap)) {
|
||||
continue;
|
||||
}
|
||||
Object currentId = bindingMap.get(idKey);
|
||||
if (expectedId != null && currentId != null && expectedId.equals(String.valueOf(currentId))) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
filtered.add(new LinkedHashMap<>((Map<String, Object>) bindingMap));
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
snapshot.put(bindingsKey, filtered);
|
||||
Bot update = new Bot();
|
||||
update.setId(botId);
|
||||
update.setPublishedSnapshotJson(snapshot);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
private <T> Set<BigInteger> collectBotIds(Collection<T> relations, BotIdGetter<T> getter) {
|
||||
if (relations == null || relations.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
for (T relation : relations) {
|
||||
BigInteger botId = getter.getBotId(relation);
|
||||
if (botId != null) {
|
||||
result.add(botId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private OfflineImpactBindingVo toBindingVo(BigInteger id, String title) {
|
||||
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
|
||||
vo.setId(id);
|
||||
vo.setTitle(title);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface BotIdGetter<T> {
|
||||
|
||||
BigInteger getBotId(T relation);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.poi.extractor.ExtractorFactory;
|
||||
import org.apache.poi.extractor.POITextExtractor;
|
||||
import org.apache.pdfbox.multipdf.Splitter;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
@@ -22,8 +24,10 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class DocUtil {
|
||||
@@ -85,6 +89,27 @@ public class DocUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取素材预览文本。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @param is 文件输入流
|
||||
* @return 预览文本
|
||||
*/
|
||||
public static String readPreviewContent(String suffix, InputStream is) {
|
||||
String normalizedSuffix = normalizeSuffix(suffix);
|
||||
if (isPlainTextSuffix(normalizedSuffix)) {
|
||||
return readPlainTextFile(is);
|
||||
}
|
||||
if ("pdf".equals(normalizedSuffix)) {
|
||||
return readPdfFile(is);
|
||||
}
|
||||
if (isOfficeSuffix(normalizedSuffix)) {
|
||||
return readOfficeFile(is);
|
||||
}
|
||||
throw new IllegalArgumentException("不支持的文件类型: " + suffix);
|
||||
}
|
||||
|
||||
public static Map<Integer, byte[]> splitPdf(byte[] bytes, int splitSize) {
|
||||
|
||||
Map<Integer, byte[]> map = new HashMap<>();
|
||||
@@ -174,6 +199,16 @@ public class DocUtil {
|
||||
return name.substring(name.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化文件后缀,统一使用小写。
|
||||
*
|
||||
* @param suffix 原始文件后缀
|
||||
* @return 规范化后的后缀
|
||||
*/
|
||||
public static String normalizeSuffix(String suffix) {
|
||||
return suffix == null ? "" : suffix.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public static byte[] readBytes(InputStream inputStream) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
try {
|
||||
@@ -194,4 +229,59 @@ public class DocUtil {
|
||||
public static String getFileNameByUrl(String url) {
|
||||
return url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取纯文本类型文件。
|
||||
*
|
||||
* @param is 文件输入流
|
||||
* @return 文本内容
|
||||
*/
|
||||
private static String readPlainTextFile(InputStream is) {
|
||||
try {
|
||||
return new String(readBytes(is), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("读取文本文件失败:", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Apache POI 提取 Office 文档中的纯文本。
|
||||
*
|
||||
* @param is 文件输入流
|
||||
* @return 文本内容
|
||||
*/
|
||||
private static String readOfficeFile(InputStream is) {
|
||||
try (POITextExtractor extractor = ExtractorFactory.createExtractor(is)) {
|
||||
return extractor.getText();
|
||||
} catch (Exception e) {
|
||||
log.error("读取 Office 文件失败:", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为纯文本类文件。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否纯文本类文件
|
||||
*/
|
||||
private static boolean isPlainTextSuffix(String suffix) {
|
||||
return "txt".equals(suffix) || "md".equals(suffix) || "csv".equals(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Office 文件。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否 Office 文件
|
||||
*/
|
||||
private static boolean isOfficeSuffix(String suffix) {
|
||||
return "doc".equals(suffix)
|
||||
|| "docx".equals(suffix)
|
||||
|| "xls".equals(suffix)
|
||||
|| "xlsx".equals(suffix)
|
||||
|| "ppt".equals(suffix)
|
||||
|| "pptx".equals(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 下线影响项视图对象。
|
||||
*/
|
||||
public class OfflineImpactBindingVo {
|
||||
|
||||
private BigInteger id;
|
||||
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 获取资源 ID。
|
||||
*
|
||||
* @return 资源 ID
|
||||
*/
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置资源 ID。
|
||||
*
|
||||
* @param id 资源 ID
|
||||
*/
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源名称。
|
||||
*
|
||||
* @return 资源名称
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置资源名称。
|
||||
*
|
||||
* @param title 资源名称
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 下线影响检查结果。
|
||||
*/
|
||||
public class OfflineImpactCheckVo {
|
||||
|
||||
private boolean canProceed;
|
||||
|
||||
private boolean hasBotBindings;
|
||||
|
||||
private boolean hasWorkflowUsages;
|
||||
|
||||
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
|
||||
|
||||
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
|
||||
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 是否允许继续下线。
|
||||
*
|
||||
* @return 是否允许继续
|
||||
*/
|
||||
public boolean isCanProceed() {
|
||||
return canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否允许继续下线。
|
||||
*
|
||||
* @param canProceed 是否允许继续
|
||||
*/
|
||||
public void setCanProceed(boolean canProceed) {
|
||||
this.canProceed = canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在 Bot 绑定。
|
||||
*
|
||||
* @return 是否存在 Bot 绑定
|
||||
*/
|
||||
public boolean isHasBotBindings() {
|
||||
return hasBotBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否存在 Bot 绑定。
|
||||
*
|
||||
* @param hasBotBindings 是否存在 Bot 绑定
|
||||
*/
|
||||
public void setHasBotBindings(boolean hasBotBindings) {
|
||||
this.hasBotBindings = hasBotBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在工作流引用。
|
||||
*
|
||||
* @return 是否存在工作流引用
|
||||
*/
|
||||
public boolean isHasWorkflowUsages() {
|
||||
return hasWorkflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否存在工作流引用。
|
||||
*
|
||||
* @param hasWorkflowUsages 是否存在工作流引用
|
||||
*/
|
||||
public void setHasWorkflowUsages(boolean hasWorkflowUsages) {
|
||||
this.hasWorkflowUsages = hasWorkflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Bot 绑定列表。
|
||||
*
|
||||
* @return Bot 绑定列表
|
||||
*/
|
||||
public List<OfflineImpactBindingVo> getBotBindings() {
|
||||
return botBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Bot 绑定列表。
|
||||
*
|
||||
* @param botBindings Bot 绑定列表
|
||||
*/
|
||||
public void setBotBindings(List<OfflineImpactBindingVo> botBindings) {
|
||||
this.botBindings = botBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流引用列表。
|
||||
*
|
||||
* @return 工作流引用列表
|
||||
*/
|
||||
public List<OfflineImpactBindingVo> getWorkflowUsages() {
|
||||
return workflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作流引用列表。
|
||||
*
|
||||
* @param workflowUsages 工作流引用列表
|
||||
*/
|
||||
public void setWorkflowUsages(List<OfflineImpactBindingVo> workflowUsages) {
|
||||
this.workflowUsages = workflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提示信息。
|
||||
*
|
||||
* @return 提示信息
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提示信息。
|
||||
*
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,15 @@ import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提审动作切面。
|
||||
@@ -25,11 +29,14 @@ import java.math.BigInteger;
|
||||
public class ApprovalActionAspect {
|
||||
|
||||
private final ApprovalActionFacade approvalActionFacade;
|
||||
private final List<ApprovalSubjectHandler> handlers;
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade) {
|
||||
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade,
|
||||
List<ApprovalSubjectHandler> handlers) {
|
||||
this.approvalActionFacade = approvalActionFacade;
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +44,7 @@ public class ApprovalActionAspect {
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param approvalAction 注解
|
||||
* @return 审批实例 ID
|
||||
* @return 动作执行结果
|
||||
* @throws Throwable 执行异常
|
||||
*/
|
||||
@Around("@annotation(approvalAction)")
|
||||
@@ -45,12 +52,14 @@ public class ApprovalActionAspect {
|
||||
Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr());
|
||||
BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier));
|
||||
joinPoint.proceed();
|
||||
return approvalActionFacade.submit(
|
||||
approvalAction.resourceType(),
|
||||
ApprovalSubjectHandler handler = getHandler(approvalAction.resourceType());
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(
|
||||
resourceId,
|
||||
approvalAction.actionType(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
ApprovalActionResult result = approvalActionFacade.submit(request);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) {
|
||||
@@ -63,4 +72,11 @@ public class ApprovalActionAspect {
|
||||
);
|
||||
return expressionParser.parseExpression(idExpr).getValue(context);
|
||||
}
|
||||
|
||||
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||
return handlers.stream()
|
||||
.filter(item -> item.resourceType().equals(resourceType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("未找到审批处理器: " + resourceType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.approval.entity.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 审批动作执行结果。
|
||||
*/
|
||||
public class ApprovalActionResult {
|
||||
|
||||
private boolean approvalRequired;
|
||||
|
||||
private BigInteger instanceId;
|
||||
|
||||
/**
|
||||
* 构造需要审批的结果。
|
||||
*
|
||||
* @param instanceId 审批实例 ID
|
||||
* @return 审批结果
|
||||
*/
|
||||
public static ApprovalActionResult required(BigInteger instanceId) {
|
||||
ApprovalActionResult result = new ApprovalActionResult();
|
||||
result.setApprovalRequired(true);
|
||||
result.setInstanceId(instanceId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造直接执行完成的结果。
|
||||
*
|
||||
* @return 审批结果
|
||||
*/
|
||||
public static ApprovalActionResult direct() {
|
||||
ApprovalActionResult result = new ApprovalActionResult();
|
||||
result.setApprovalRequired(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isApprovalRequired() {
|
||||
return approvalRequired;
|
||||
}
|
||||
|
||||
public void setApprovalRequired(boolean approvalRequired) {
|
||||
this.approvalRequired = approvalRequired;
|
||||
}
|
||||
|
||||
public BigInteger getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public void setInstanceId(BigInteger instanceId) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ public class ApprovalInstanceDetailVo {
|
||||
|
||||
private String applicantName;
|
||||
|
||||
private String applicantAccount;
|
||||
|
||||
private Date submittedAt;
|
||||
|
||||
private Date finishedAt;
|
||||
@@ -137,6 +139,14 @@ public class ApprovalInstanceDetailVo {
|
||||
this.applicantName = applicantName;
|
||||
}
|
||||
|
||||
public String getApplicantAccount() {
|
||||
return applicantAccount;
|
||||
}
|
||||
|
||||
public void setApplicantAccount(String applicantAccount) {
|
||||
this.applicantAccount = applicantAccount;
|
||||
}
|
||||
|
||||
public Date getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class ApprovalLogVo {
|
||||
|
||||
private BigInteger operatorId;
|
||||
|
||||
private String operatorAccount;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private Date created;
|
||||
@@ -45,6 +47,14 @@ public class ApprovalLogVo {
|
||||
this.operatorId = operatorId;
|
||||
}
|
||||
|
||||
public String getOperatorAccount() {
|
||||
return operatorAccount;
|
||||
}
|
||||
|
||||
public void setOperatorAccount(String operatorAccount) {
|
||||
this.operatorAccount = operatorAccount;
|
||||
}
|
||||
|
||||
public String getOperatorName() {
|
||||
return operatorName;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.Locale;
|
||||
public enum ApprovalActionType {
|
||||
|
||||
PUBLISH("PUBLISH"),
|
||||
OFFLINE("OFFLINE"),
|
||||
DELETE("DELETE");
|
||||
|
||||
private final String code;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,13 +14,10 @@ public interface ApprovalActionFacade {
|
||||
/**
|
||||
* 提交审批。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 审批实例 ID
|
||||
* @param request 审批提交请求
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
ApprovalActionResult submit(ApprovalSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 处理审批通过后的业务回调。
|
||||
|
||||
@@ -15,4 +15,12 @@ public interface ApprovalMatchService {
|
||||
* @return 命中的流程详情
|
||||
*/
|
||||
ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 根据资源上下文匹配审批流程,未命中时返回 {@code null}。
|
||||
*
|
||||
* @param request 审批提交请求
|
||||
* @return 命中的流程详情,未命中时返回 {@code null}
|
||||
*/
|
||||
ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 审批结果业务处理器。
|
||||
*/
|
||||
public interface ApprovalResultHandler {
|
||||
|
||||
/**
|
||||
* 处理审批通过后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
|
||||
/**
|
||||
* 处理审批驳回后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
|
||||
/**
|
||||
* 处理审批撤回后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -28,34 +26,6 @@ public interface ApprovalSubjectHandler {
|
||||
*/
|
||||
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 审批提交完成后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onSubmitted(ApprovalSubmitCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批通过后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onApproved(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批驳回后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onRejected(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批撤回后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onRevoked(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 校验资源是否已发布。
|
||||
*
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -22,31 +23,30 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
|
||||
private final List<ApprovalSubjectHandler> handlers;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final ApprovalMatchService approvalMatchService;
|
||||
private final ApprovalResultHandler approvalResultHandler;
|
||||
|
||||
public ApprovalActionFacadeImpl(List<ApprovalSubjectHandler> handlers,
|
||||
ApprovalInstanceService approvalInstanceService) {
|
||||
ApprovalInstanceService approvalInstanceService,
|
||||
ApprovalMatchService approvalMatchService,
|
||||
ApprovalResultHandler approvalResultHandler) {
|
||||
this.handlers = handlers;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.approvalMatchService = approvalMatchService;
|
||||
this.approvalResultHandler = approvalResultHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
ApprovalSubjectHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
public ApprovalActionResult submit(ApprovalSubmitRequest request) {
|
||||
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||
if (flow == null) {
|
||||
return ApprovalActionResult.direct();
|
||||
}
|
||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||
|
||||
ApprovalSubmitCallbackContext context = new ApprovalSubmitCallbackContext();
|
||||
context.setInstanceId(instanceId);
|
||||
context.setResourceType(request.getResourceType());
|
||||
context.setResourceId(request.getResourceId());
|
||||
context.setActionType(request.getActionType());
|
||||
context.setOperatorId(operatorId);
|
||||
handler.onSubmitted(context);
|
||||
return instanceId;
|
||||
return ApprovalActionResult.required(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +54,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onApproved(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleApproved(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,8 +62,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onRejected(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleRejected(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,8 +70,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onRevoked(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleRevoked(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,14 +82,6 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
handler.assertPublishedAccess(identifier, denyMessage);
|
||||
}
|
||||
|
||||
private ApprovalCallbackContext buildCallbackContext(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalCallbackContext context = new ApprovalCallbackContext();
|
||||
context.setInstance(instance);
|
||||
context.setOperatorId(operatorId);
|
||||
context.setComment(comment);
|
||||
return context;
|
||||
}
|
||||
|
||||
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||
String normalized = ApprovalResourceType.from(resourceType).getCode();
|
||||
return handlers.stream()
|
||||
|
||||
@@ -53,6 +53,18 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
*/
|
||||
@Override
|
||||
public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) {
|
||||
ApprovalFlowDetailVo matchedFlow = matchFlowOrNull(request);
|
||||
if (matchedFlow == null) {
|
||||
throw new BusinessException("当前资源上下文未命中审批流程");
|
||||
}
|
||||
return matchedFlow;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request) {
|
||||
ApprovalSubmitRequest normalized = normalizeRequest(request);
|
||||
QueryWrapper flowWrapper = QueryWrapper.create()
|
||||
.eq(ApprovalFlow::getResourceType, normalized.getResourceType())
|
||||
@@ -60,7 +72,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
.eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode());
|
||||
List<ApprovalFlow> flows = approvalFlowMapper.selectListByQuery(flowWrapper);
|
||||
if (CollectionUtil.isEmpty(flows)) {
|
||||
throw new BusinessException("未找到可用的审批流程");
|
||||
return null;
|
||||
}
|
||||
List<BigInteger> flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList());
|
||||
Map<BigInteger, List<ApprovalFlowScope>> scopeMap = approvalFlowScopeMapper.selectListByQuery(
|
||||
@@ -76,7 +88,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
}
|
||||
}
|
||||
if (matchedFlows.isEmpty()) {
|
||||
throw new BusinessException("当前资源上下文未命中审批流程");
|
||||
return null;
|
||||
}
|
||||
|
||||
matchedFlows.sort(Comparator
|
||||
|
||||
@@ -154,8 +154,9 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
||||
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
||||
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
||||
Map<BigInteger, String> accountNameMap = loadAccountNameMap(instance, tasks, logs);
|
||||
detail.setApplicantName(accountNameMap.get(instance.getApplicantId()));
|
||||
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs);
|
||||
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
|
||||
detail.setApplicantAccount(resolveAccountLoginName(accountMap.get(instance.getApplicantId())));
|
||||
|
||||
detail.setTasks(tasks.stream()
|
||||
.sorted(Comparator.comparing(ApprovalTask::getStepNo))
|
||||
@@ -171,7 +172,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
||||
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
||||
taskVo.setActedBy(item.getActedBy());
|
||||
taskVo.setActedByName(accountNameMap.get(item.getActedBy()));
|
||||
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
|
||||
taskVo.setActedAt(item.getActedAt());
|
||||
taskVo.setComment(item.getComment());
|
||||
return taskVo;
|
||||
@@ -185,7 +186,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
logVo.setId(item.getId());
|
||||
logVo.setEventType(item.getEventType());
|
||||
logVo.setOperatorId(item.getOperatorId());
|
||||
logVo.setOperatorName(accountNameMap.get(item.getOperatorId()));
|
||||
logVo.setOperatorAccount(resolveAccountLoginName(accountMap.get(item.getOperatorId())));
|
||||
logVo.setOperatorName(resolveAccountName(accountMap.get(item.getOperatorId())));
|
||||
logVo.setCreated(item.getCreated());
|
||||
logVo.setPayloadJson(item.getPayloadJson());
|
||||
return logVo;
|
||||
@@ -205,14 +207,14 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载审批详情里涉及到的账号显示名称。
|
||||
* 批量加载审批详情里涉及到的账号信息。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param tasks 审批任务列表
|
||||
* @param logs 审批日志列表
|
||||
* @return 账号 ID 到展示名称的映射
|
||||
* @return 账号 ID 到账号实体的映射
|
||||
*/
|
||||
private Map<BigInteger, String> loadAccountNameMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
||||
private Map<BigInteger, SysAccount> loadAccountMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
||||
List<ApprovalLog> logs) {
|
||||
Set<BigInteger> accountIds = new HashSet<>();
|
||||
if (instance.getApplicantId() != null) {
|
||||
@@ -232,7 +234,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
return sysAccountService.listByIds(accountIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
SysAccount::getId,
|
||||
this::resolveAccountName,
|
||||
account -> account,
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
@@ -256,6 +258,19 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析账号登录名。
|
||||
*
|
||||
* @param account 账号实体
|
||||
* @return 登录账号
|
||||
*/
|
||||
private String resolveAccountLoginName(SysAccount account) {
|
||||
if (account == null || !StringUtils.hasText(account.getLoginName())) {
|
||||
return null;
|
||||
}
|
||||
return account.getLoginName().trim();
|
||||
}
|
||||
|
||||
private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
if (StringUtils.hasText(resourceType)) {
|
||||
|
||||
@@ -15,7 +15,11 @@ import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -32,6 +36,16 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
||||
@Resource
|
||||
private SysAccountRoleService sysAccountRoleService;
|
||||
|
||||
/**
|
||||
* 根据账号查询菜单,并自动补齐已授权节点的父级菜单链。
|
||||
* <p>
|
||||
* 这样当角色只勾选了某个页面下的按钮权限或子能力时,
|
||||
* 其所属的页面菜单仍能正常出现在侧边栏中,避免出现“有子权限但无入口”的问题。
|
||||
*
|
||||
* @param entity 菜单过滤条件
|
||||
* @param accountId 账号 ID
|
||||
* @return 当前账号可访问的菜单集合
|
||||
*/
|
||||
@Override
|
||||
public List<SysMenu> getMenusByAccountId(SysMenu entity, BigInteger accountId) {
|
||||
// 查询用户对应角色id集合
|
||||
@@ -48,11 +62,53 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
||||
if (CollectionUtil.isEmpty(menuIds)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<BigInteger> fullMenuIds = collectMenuIdsWithParents(menuIds);
|
||||
// 查询当前用户拥有的菜单
|
||||
SqlOperators ops = SqlOperatorsUtil.build(SysMenu.class);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, ops);
|
||||
queryWrapper.in(SysMenu::getId, menuIds);
|
||||
queryWrapper.in(SysMenu::getId, fullMenuIds);
|
||||
queryWrapper.orderBy("sort_no asc");
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集菜单自身及其所有父级菜单 ID。
|
||||
*
|
||||
* @param menuIds 已授权的菜单 ID 列表
|
||||
* @return 包含父级链路的完整菜单 ID 列表
|
||||
*/
|
||||
private List<BigInteger> collectMenuIdsWithParents(List<BigInteger> menuIds) {
|
||||
List<SysMenu> allMenus = list();
|
||||
if (CollectionUtil.isEmpty(allMenus)) {
|
||||
return menuIds;
|
||||
}
|
||||
Map<BigInteger, SysMenu> menuMap = new HashMap<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
menuMap.put(menu.getId(), menu);
|
||||
}
|
||||
Set<BigInteger> result = new HashSet<>(menuIds);
|
||||
for (BigInteger menuId : menuIds) {
|
||||
appendParentMenuIds(menuId, menuMap, result);
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归追加父级菜单 ID,直到根节点或无父节点为止。
|
||||
*
|
||||
* @param menuId 当前菜单 ID
|
||||
* @param menuMap 全量菜单映射
|
||||
* @param result 结果集合
|
||||
*/
|
||||
private void appendParentMenuIds(BigInteger menuId, Map<BigInteger, SysMenu> menuMap, Set<BigInteger> result) {
|
||||
SysMenu current = menuMap.get(menuId);
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
BigInteger parentId = current.getParentId();
|
||||
if (parentId == null || BigInteger.ZERO.equals(parentId) || !result.add(parentId)) {
|
||||
return;
|
||||
}
|
||||
appendParentMenuIds(parentId, menuMap, result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user