feat: 收敛AI资源发布审批生命周期

- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

- 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览

- 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
2026-04-09 17:13:54 +08:00
parent 81125ce55c
commit 4e565aef99
68 changed files with 3859 additions and 817 deletions

View File

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

View File

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

View File

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

View File

@@ -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;
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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 -> "公开";
};
}
}

View File

@@ -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) {

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import java.util.Locale;
public enum ApprovalActionType {
PUBLISH("PUBLISH"),
OFFLINE("OFFLINE"),
DELETE("DELETE");
private final String code;

View File

@@ -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);
/**
* 处理审批通过后的业务回调。

View File

@@ -15,4 +15,12 @@ public interface ApprovalMatchService {
* @return 命中的流程详情
*/
ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request);
/**
* 根据资源上下文匹配审批流程,未命中时返回 {@code null}。
*
* @param request 审批提交请求
* @return 命中的流程详情,未命中时返回 {@code null}
*/
ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request);
}

View File

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

View File

@@ -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);
/**
* 校验资源是否已发布。
*

View File

@@ -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()

View File

@@ -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

View File

@@ -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)) {

View File

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