feat: 收敛AI资源发布审批生命周期
- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
@@ -19,6 +19,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.service.*;
|
||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||
@@ -70,6 +71,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private BotPublishAppService botPublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
|
||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||
@@ -197,6 +200,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) {
|
||||
throw new BusinessException("聊天助手尚未发布");
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
||||
}
|
||||
return Result.ok(bot);
|
||||
}
|
||||
|
||||
@@ -221,6 +227,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
}
|
||||
|
||||
if (data.getModelId() == null) {
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@@ -229,6 +238,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
|
||||
if (llm == null) {
|
||||
data.setModelId(null);
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@@ -242,19 +254,40 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
|
||||
}
|
||||
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(botPublishAppService.submitPublishApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
botPublishAppService.submitPublishApproval(id),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(
|
||||
botPublishAppService.submitOfflineApproval(id),
|
||||
"已提交下线审批",
|
||||
"已直接下线"
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/bot/remove")
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(botPublishAppService.submitDeleteApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
botPublishAppService.submitDeleteApproval(id),
|
||||
"已提交删除审批",
|
||||
"已直接删除"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -262,13 +295,26 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
applyCategoryPermission(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
List<Bot> bots = service.list(queryWrapper);
|
||||
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
||||
return Result.ok(bots);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(
|
||||
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||
actionResult.getInstanceId()
|
||||
);
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
@@ -388,7 +434,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.fail(1, "请提交删除审批");
|
||||
return Result.fail(1, "请使用发布状态操作删除聊天助手");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,9 @@ import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.KnowledgePublishAppService;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
@@ -71,6 +74,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||
@Resource
|
||||
private KnowledgePublishAppService knowledgePublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
|
||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||
super(service);
|
||||
@@ -177,6 +182,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
)
|
||||
public Result<DocumentCollection> detail(String id) {
|
||||
DocumentCollection detail = service.getDetail(id);
|
||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(detail);
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
@@ -195,7 +201,46 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.submitPublishApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitPublishApproval(id),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitOfflineApproval(id),
|
||||
"已提交下线审批",
|
||||
"已直接下线"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查知识库下线影响。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
@GetMapping("/offlineImpactCheck")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.checkOfflineImpact(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +252,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.submitDeleteApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitDeleteApproval(id),
|
||||
"已提交删除审批",
|
||||
"已直接删除"
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("splitterProfile/save")
|
||||
@@ -250,20 +299,33 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
List<DocumentCollection> collections = service.list(queryWrapper);
|
||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(collections);
|
||||
return Result.ok(collections);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
|
||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.fail(1, "请提交删除审批");
|
||||
return Result.fail(1, "请使用发布状态操作删除知识库");
|
||||
}
|
||||
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(
|
||||
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||
actionResult.getInstanceId()
|
||||
);
|
||||
}
|
||||
|
||||
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
|
||||
|
||||
@@ -4,14 +4,19 @@ import cn.hutool.core.io.FileTypeUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.admin.model.ai.ResourcePreviewVo;
|
||||
import tech.easyflow.ai.entity.Resource;
|
||||
import tech.easyflow.ai.service.ResourceService;
|
||||
import tech.easyflow.ai.utils.DocUtil;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
@@ -31,6 +36,8 @@ import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource")
|
||||
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||
private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000;
|
||||
|
||||
@javax.annotation.Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@@ -79,6 +86,26 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
return Result.ok(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取素材预览内容。
|
||||
*
|
||||
* @param id 素材ID
|
||||
* @return 预览文本
|
||||
*/
|
||||
@GetMapping("/previewContent")
|
||||
public Result<ResourcePreviewVo> previewContent(String id) {
|
||||
Resource resource = service.getById(id);
|
||||
if (resource == null) {
|
||||
throw new BusinessException("素材不存在");
|
||||
}
|
||||
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||
|
||||
byte[] bytes = DocUtil.downloadFile(resource.getResourceUrl());
|
||||
String suffix = resolvePreviewSuffix(resource, bytes);
|
||||
String content = DocUtil.readPreviewContent(suffix, new ByteArrayInputStream(bytes));
|
||||
return Result.ok(buildPreviewVo(content));
|
||||
}
|
||||
|
||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||
if (!access.isRestricted()) {
|
||||
@@ -90,4 +117,37 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
}
|
||||
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用于预览的文件后缀。
|
||||
*
|
||||
* @param resource 素材实体
|
||||
* @param bytes 文件字节
|
||||
* @return 标准化后的文件后缀
|
||||
*/
|
||||
private String resolvePreviewSuffix(Resource resource, byte[] bytes) {
|
||||
if (StringUtils.hasText(resource.getSuffix())) {
|
||||
return DocUtil.normalizeSuffix(resource.getSuffix());
|
||||
}
|
||||
String detectedSuffix = FileTypeUtil.getType(new ByteArrayInputStream(bytes), resource.getResourceUrl());
|
||||
if (!StringUtils.hasText(detectedSuffix)) {
|
||||
throw new BusinessException("无法识别当前素材文件类型");
|
||||
}
|
||||
return DocUtil.normalizeSuffix(detectedSuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建预览返回对象,并在服务端截断超长内容。
|
||||
*
|
||||
* @param content 原始内容
|
||||
* @return 预览内容对象
|
||||
*/
|
||||
private ResourcePreviewVo buildPreviewVo(String content) {
|
||||
String safeContent = content == null ? "" : content;
|
||||
boolean truncated = safeContent.length() > RESOURCE_PREVIEW_CONTENT_LIMIT;
|
||||
ResourcePreviewVo vo = new ResourcePreviewVo();
|
||||
vo.setContent(truncated ? safeContent.substring(0, RESOURCE_PREVIEW_CONTENT_LIMIT) : safeContent);
|
||||
vo.setTruncated(truncated);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
@@ -88,6 +91,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
@Resource
|
||||
private WorkflowPublishAppService workflowPublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
@@ -254,7 +259,46 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.submitPublishApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitPublishApproval(id),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitOfflineApproval(id),
|
||||
"已提交下线审批",
|
||||
"已直接下线"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工作流下线影响。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
@GetMapping("/offlineImpactCheck")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限管理工作流"
|
||||
)
|
||||
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.checkOfflineImpact(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,7 +310,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/remove")
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.submitDeleteApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitDeleteApproval(id),
|
||||
"已提交删除审批",
|
||||
"已直接删除"
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/supportedCodeEngines")
|
||||
@@ -307,6 +355,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
)
|
||||
public Result<Workflow> detail(String id) {
|
||||
Workflow workflow = service.getDetail(id);
|
||||
aiResourceApprovalStateService.fillWorkflowApprovalState(workflow);
|
||||
return Result.ok(workflow);
|
||||
}
|
||||
|
||||
@@ -368,20 +417,33 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
List<Workflow> workflows = service.list(queryWrapper);
|
||||
aiResourceApprovalStateService.fillWorkflowApprovalState(workflows);
|
||||
return Result.ok(workflows);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.fail(1, "请提交删除审批");
|
||||
return Result.fail(1, "请使用发布状态操作删除工作流");
|
||||
}
|
||||
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(
|
||||
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||
actionResult.getInstanceId()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package tech.easyflow.admin.model.ai;
|
||||
|
||||
/**
|
||||
* 素材预览内容返回对象。
|
||||
*/
|
||||
public class ResourcePreviewVo {
|
||||
|
||||
private String content;
|
||||
|
||||
private boolean truncated;
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public boolean isTruncated() {
|
||||
return truncated;
|
||||
}
|
||||
|
||||
public void setTruncated(boolean truncated) {
|
||||
this.truncated = truncated;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import tech.easyflow.ai.entity.base.BotBase;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
@@ -19,6 +20,15 @@ public class Bot extends BotBase {
|
||||
public static final String KEY_MAX_MESSAGE_COUNT = "maxMessageCount";
|
||||
public static final String KEY_ENABLE_DEEP_THINKING = "enableDeepThinking";
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public boolean isAnonymousEnabled() {
|
||||
Map<String, Object> options = getOptions();
|
||||
if (options == null) {
|
||||
@@ -28,4 +38,28 @@ public class Bot extends BotBase {
|
||||
return o != null && (boolean) o;
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package tech.easyflow.ai.entity;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.easyagents.store.milvus.MilvusVectorStore;
|
||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
@@ -27,6 +28,15 @@ import java.util.Map;
|
||||
@Table("tb_document_collection")
|
||||
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||
public static final String TYPE_FAQ = "FAQ";
|
||||
|
||||
@@ -135,4 +145,28 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
}
|
||||
return options.get(key);
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
@@ -16,6 +17,15 @@ import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
@Table("tb_workflow")
|
||||
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new WorkflowTool(this, needEnglishName);
|
||||
}
|
||||
@@ -23,4 +33,28 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
public Tool toFunction(boolean needEnglishName, String definitionId) {
|
||||
return new WorkflowTool(this, needEnglishName, definitionId);
|
||||
}
|
||||
|
||||
public Boolean getApprovalPending() {
|
||||
return approvalPending;
|
||||
}
|
||||
|
||||
public void setApprovalPending(Boolean approvalPending) {
|
||||
this.approvalPending = approvalPending;
|
||||
}
|
||||
|
||||
public String getCurrentApprovalActionType() {
|
||||
return currentApprovalActionType;
|
||||
}
|
||||
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||
this.currentApprovalActionType = currentApprovalActionType;
|
||||
}
|
||||
|
||||
public String getDisplayPublishStatus() {
|
||||
return displayPublishStatus;
|
||||
}
|
||||
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public enum PublishStatus {
|
||||
DRAFT("DRAFT"),
|
||||
PUBLISH_PENDING("PUBLISH_PENDING"),
|
||||
PUBLISHED("PUBLISHED"),
|
||||
OFFLINE_PENDING("OFFLINE_PENDING"),
|
||||
OFFLINE("OFFLINE"),
|
||||
DELETE_PENDING("DELETE_PENDING");
|
||||
|
||||
private final String code;
|
||||
@@ -34,7 +36,16 @@ public enum PublishStatus {
|
||||
* @return 允许外部访问或线上运行时返回 true
|
||||
*/
|
||||
public boolean isExternallyVisible() {
|
||||
return this == PUBLISHED || this == DELETE_PENDING;
|
||||
return this == PUBLISHED || this == DELETE_PENDING || this == OFFLINE_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否允许作为 Bot 新绑定候选。
|
||||
*
|
||||
* @return 仅已发布状态返回 true
|
||||
*/
|
||||
public boolean isSelectableForBot() {
|
||||
return this == PUBLISHED;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期处理器抽象基类。
|
||||
*
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
public abstract class AbstractAiResourceLifecycleHandler<T> implements ApprovalSubjectHandler, AiResourceLifecycleHandler {
|
||||
|
||||
protected static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
protected static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
protected final ObjectMapper objectMapper;
|
||||
|
||||
protected AbstractAiResourceLifecycleHandler(ApprovalInstanceService approvalInstanceService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资源,不存在时抛异常。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @return 资源
|
||||
*/
|
||||
protected abstract T requireResource(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 校验资源管理权限。
|
||||
*
|
||||
* @param resource 资源
|
||||
*/
|
||||
protected abstract void assertManagePermission(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源分类 ID。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 分类 ID
|
||||
*/
|
||||
protected abstract BigInteger getCategoryId(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源部门 ID。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 部门 ID
|
||||
*/
|
||||
protected abstract BigInteger getDeptId(T resource);
|
||||
|
||||
/**
|
||||
* 获取资源标题。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 资源标题
|
||||
*/
|
||||
protected abstract String getTitle(T resource);
|
||||
|
||||
/**
|
||||
* 获取当前发布状态。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 发布状态
|
||||
*/
|
||||
protected abstract PublishStatus getCurrentStatus(T resource);
|
||||
|
||||
/**
|
||||
* 获取当前已发布快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 已发布快照
|
||||
*/
|
||||
protected abstract Map<String, Object> getPublishedSnapshot(T resource);
|
||||
|
||||
/**
|
||||
* 构建当前草稿快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 草稿快照
|
||||
*/
|
||||
protected abstract Map<String, Object> buildResourceSnapshot(T resource);
|
||||
|
||||
/**
|
||||
* 更新资源待审批状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param publishStatus 发布状态
|
||||
* @param currentApprovalInstanceId 审批实例 ID
|
||||
*/
|
||||
protected abstract void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId);
|
||||
|
||||
/**
|
||||
* 发布成功后写入已发布状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 已发布快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
protected abstract void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 下线成功后写入下线状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected abstract void markResourceOffline(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 删除资源。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected abstract void removeResource(BigInteger resourceId);
|
||||
|
||||
/**
|
||||
* 返回资源中文名称。
|
||||
*
|
||||
* @return 资源名称
|
||||
*/
|
||||
protected abstract String resourceLabel();
|
||||
|
||||
/**
|
||||
* 返回当前动作摘要前缀。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @return 摘要前缀
|
||||
*/
|
||||
protected String resolveActionLabel(ApprovalActionType actionType) {
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> "发布";
|
||||
case OFFLINE -> "下线";
|
||||
case DELETE -> "删除";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展下线快照内容。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param snapshot 当前快照副本
|
||||
*/
|
||||
protected void enrichOfflineSnapshot(T resource, Map<String, Object> snapshot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前额外校验。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
*/
|
||||
protected void validateDelete(T resource, PublishStatus currentStatus) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 下线成功后的额外副作用。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除成功前的额外副作用。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
*/
|
||||
protected void beforeRemove(BigInteger resourceId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
T resource = requireResource(resourceId);
|
||||
assertManagePermission(resource);
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "存在未结束审批,请先处理完成");
|
||||
}
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
PublishStatus currentStatus = getCurrentStatus(resource);
|
||||
Map<String, Object> resourceSnapshot = resolveSubmitSnapshot(resource, approvalActionType, currentStatus);
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(getCategoryId(resource));
|
||||
request.setDeptId(getDeptId(resource));
|
||||
request.setSummary(resolveActionLabel(approvalActionType) + resourceLabel() + ":" + getTitle(resource));
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put(SNAPSHOT_KEY, resourceSnapshot);
|
||||
snapshot.put(PREVIOUS_STATUS_KEY, currentStatus.getCode());
|
||||
request.setSnapshotJson(snapshot);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析动作对应的提交快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param actionType 动作类型
|
||||
* @param currentStatus 当前状态
|
||||
* @return 提交快照
|
||||
*/
|
||||
protected Map<String, Object> resolveSubmitSnapshot(T resource,
|
||||
ApprovalActionType actionType,
|
||||
PublishStatus currentStatus) {
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> buildPublishSnapshot(resource, currentStatus);
|
||||
case OFFLINE -> buildOfflineSnapshot(resource, currentStatus);
|
||||
case DELETE -> buildDeleteSnapshot(resource, currentStatus);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建发布快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 发布快照
|
||||
*/
|
||||
protected Map<String, Object> buildPublishSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus != PublishStatus.DRAFT
|
||||
&& currentStatus != PublishStatus.OFFLINE
|
||||
&& currentStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "状态不允许发布");
|
||||
}
|
||||
Map<String, Object> snapshot = buildResourceSnapshot(resource);
|
||||
if (currentStatus == PublishStatus.PUBLISHED && isSameSnapshot(snapshot, getPublishedSnapshot(resource))) {
|
||||
throw new BusinessException("当前内容与已发布版本一致,无需重新发布");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建下线快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 下线快照
|
||||
*/
|
||||
protected Map<String, Object> buildOfflineSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "尚未发布,无法下线");
|
||||
}
|
||||
Map<String, Object> publishedSnapshot = getPublishedSnapshot(resource);
|
||||
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "缺少已发布快照,无法下线");
|
||||
}
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>(publishedSnapshot);
|
||||
enrichOfflineSnapshot(resource, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建删除快照。
|
||||
*
|
||||
* @param resource 资源
|
||||
* @param currentStatus 当前状态
|
||||
* @return 删除快照
|
||||
*/
|
||||
protected Map<String, Object> buildDeleteSnapshot(T resource, PublishStatus currentStatus) {
|
||||
if (currentStatus == PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "已发布,请先下线后再删除");
|
||||
}
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
throw new BusinessException("当前" + resourceLabel() + "存在进行中的审批,请先处理完成");
|
||||
}
|
||||
validateDelete(resource, currentStatus);
|
||||
return buildResourceSnapshot(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行审批通过后的真实动作。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
@Override
|
||||
public void applyApprovedAction(String actionType,
|
||||
BigInteger resourceId,
|
||||
Map<String, Object> resourceSnapshot,
|
||||
BigInteger operatorId) {
|
||||
ApprovalActionType normalizedAction = ApprovalActionType.from(actionType);
|
||||
if (normalizedAction == ApprovalActionType.PUBLISH) {
|
||||
publishResource(resourceId, resourceSnapshot, operatorId);
|
||||
return;
|
||||
}
|
||||
if (normalizedAction == ApprovalActionType.OFFLINE) {
|
||||
markResourceOffline(resourceId);
|
||||
afterOffline(resourceId);
|
||||
return;
|
||||
}
|
||||
beforeRemove(resourceId);
|
||||
removeResource(resourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId) {
|
||||
persistResourceState(resourceId, publishStatus, instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void restoreState(BigInteger resourceId, PublishStatus previousStatus) {
|
||||
persistResourceState(resourceId, previousStatus, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求快照读取资源内容。
|
||||
*
|
||||
* @param snapshotJson 请求快照
|
||||
* @return 资源快照
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少" + resourceLabel() + "内容");
|
||||
}
|
||||
return new LinkedHashMap<>((Map<String, Object>) map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前草稿快照与已发布快照是否一致。
|
||||
*
|
||||
* @param currentSnapshot 当前快照
|
||||
* @param publishedSnapshot 已发布快照
|
||||
* @return 一致返回 true
|
||||
*/
|
||||
protected boolean isSameSnapshot(Map<String, Object> currentSnapshot, Map<String, Object> publishedSnapshot) {
|
||||
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(
|
||||
objectMapper.valueToTree(currentSnapshot),
|
||||
objectMapper.valueToTree(publishedSnapshot)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期处理器。
|
||||
*/
|
||||
public interface AiResourceLifecycleHandler {
|
||||
|
||||
/**
|
||||
* 当前处理器支持的资源类型。
|
||||
*
|
||||
* @return 资源类型编码
|
||||
*/
|
||||
String resourceType();
|
||||
|
||||
/**
|
||||
* 构建动作提交请求。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 提交请求
|
||||
*/
|
||||
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 写入待审批状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param publishStatus 待写入的发布状态
|
||||
* @param instanceId 审批实例 ID
|
||||
*/
|
||||
void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId);
|
||||
|
||||
/**
|
||||
* 执行动作真正生效后的资源更新与副作用。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param resourceSnapshot 审批冻结快照
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
void applyApprovedAction(String actionType, BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 按提交前真实状态恢复资源状态。
|
||||
*
|
||||
* @param resourceId 资源 ID
|
||||
* @param previousStatus 提交前状态
|
||||
*/
|
||||
void restoreState(BigInteger resourceId, PublishStatus previousStatus);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期统一状态机服务。
|
||||
*/
|
||||
public interface AiResourceLifecycleService {
|
||||
|
||||
/**
|
||||
* 提交资源动作。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 执行结果
|
||||
*/
|
||||
ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 资源生命周期统一状态机实现。
|
||||
*/
|
||||
@Service
|
||||
public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleService, ApprovalResultHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
private static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||
|
||||
private final List<AiResourceLifecycleHandler> handlers;
|
||||
private final ApprovalMatchService approvalMatchService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
|
||||
public AiResourceLifecycleServiceImpl(List<AiResourceLifecycleHandler> handlers,
|
||||
ApprovalMatchService approvalMatchService,
|
||||
ApprovalInstanceService approvalInstanceService) {
|
||||
this.handlers = handlers;
|
||||
this.approvalMatchService = approvalMatchService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
AiResourceLifecycleHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||
if (flow == null) {
|
||||
handler.applyApprovedAction(actionType, resourceId, readResourceSnapshot(request.getSnapshotJson()), operatorId);
|
||||
return ApprovalActionResult.direct();
|
||||
}
|
||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||
handler.updatePendingState(
|
||||
resourceId,
|
||||
resolveSubmittedStatus(actionType, resolvePreviousStatus(request.getSnapshotJson())),
|
||||
instanceId
|
||||
);
|
||||
return ApprovalActionResult.required(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).applyApprovedAction(
|
||||
instance.getActionType(),
|
||||
instance.getResourceId(),
|
||||
readResourceSnapshot(instance.getSnapshotJson()),
|
||||
operatorId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).restoreState(
|
||||
instance.getResourceId(),
|
||||
resolvePreviousStatus(instance.getSnapshotJson())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
getHandler(instance.getResourceType()).restoreState(
|
||||
instance.getResourceId(),
|
||||
resolvePreviousStatus(instance.getSnapshotJson())
|
||||
);
|
||||
}
|
||||
|
||||
private AiResourceLifecycleHandler getHandler(String resourceType) {
|
||||
return handlers.stream()
|
||||
.filter(item -> item.resourceType().equals(resourceType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("未找到资源生命周期处理器: " + resourceType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析提交动作后的真实持久化状态。
|
||||
*
|
||||
* @param actionType 动作类型
|
||||
* @param previousStatus 提交前真实状态
|
||||
* @return 提交后应写入的状态
|
||||
*/
|
||||
private PublishStatus resolveSubmittedStatus(String actionType, PublishStatus previousStatus) {
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(actionType) && previousStatus == PublishStatus.PUBLISHED) {
|
||||
return PublishStatus.PUBLISHED;
|
||||
}
|
||||
if (ApprovalActionType.DELETE.getCode().equals(actionType)) {
|
||||
return PublishStatus.DELETE_PENDING;
|
||||
}
|
||||
if (ApprovalActionType.OFFLINE.getCode().equals(actionType)) {
|
||||
return PublishStatus.OFFLINE_PENDING;
|
||||
}
|
||||
return PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从审批快照解析提交前真实状态。
|
||||
*
|
||||
* @param snapshotJson 审批冻结快照
|
||||
* @return 提交前真实状态
|
||||
*/
|
||||
private PublishStatus resolvePreviousStatus(Map<String, Object> snapshotJson) {
|
||||
Object status = snapshotJson == null ? null : snapshotJson.get(PREVIOUS_STATUS_KEY);
|
||||
if (status instanceof String value && !value.isBlank()) {
|
||||
return PublishStatus.from(value);
|
||||
}
|
||||
return PublishStatus.DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从审批快照中提取冻结资源内容。
|
||||
*
|
||||
* @param snapshotJson 审批冻结快照
|
||||
* @return 冻结资源内容
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少资源内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotCategory;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
@@ -26,35 +26,27 @@ import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 聊天助手审批处理器。
|
||||
* 聊天助手生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class BotApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Bot> {
|
||||
|
||||
private final BotService botService;
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
@@ -63,7 +55,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private final BotMcpService botMcpService;
|
||||
private final WorkflowService workflowService;
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ModelService modelService;
|
||||
private final BotCategoryService botCategoryService;
|
||||
@@ -84,7 +75,9 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
BotCategoryService botCategoryService,
|
||||
SysDeptService sysDeptService,
|
||||
PluginItemService pluginItemService,
|
||||
McpService mcpService) {
|
||||
McpService mcpService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.botService = botService;
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
@@ -92,7 +85,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
this.botMcpService = botMcpService;
|
||||
this.workflowService = workflowService;
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.modelService = modelService;
|
||||
this.botCategoryService = botCategoryService;
|
||||
@@ -106,69 +98,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.BOT.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
Bot bot = requireBot(resourceId);
|
||||
assertManagePermission(bot);
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前聊天助手存在未结束审批,请先处理完成");
|
||||
}
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(bot);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(bot.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前聊天助手没有变更,无需重复发布");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(bot.getCategoryId());
|
||||
request.setDeptId(bot.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "聊天助手:" + bot.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
Bot update = new Bot();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
Bot update = new Bot();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
botService.updateById(update);
|
||||
return;
|
||||
}
|
||||
removeBotRelations(instance.getResourceId());
|
||||
botService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Bot bot = botService.getDetail(String.valueOf(identifier));
|
||||
@@ -178,24 +107,52 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private Bot requireBot(BigInteger id) {
|
||||
Bot bot = botService.getById(id);
|
||||
@Override
|
||||
protected Bot requireResource(BigInteger resourceId) {
|
||||
Bot bot = botService.getById(resourceId);
|
||||
if (bot == null) {
|
||||
throw new BusinessException("聊天助手不存在");
|
||||
}
|
||||
return bot;
|
||||
}
|
||||
|
||||
private void assertManagePermission(Bot bot) {
|
||||
@Override
|
||||
protected void assertManagePermission(Bot resource) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin();
|
||||
boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy());
|
||||
boolean creator = account != null && account.getId() != null && account.getId().equals(resource.getCreatedBy());
|
||||
if (!superAdmin && !creator) {
|
||||
throw new BusinessException("仅创建者或超级管理员可管理聊天助手");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(Bot bot) {
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Bot resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Bot resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Bot resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Bot resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Bot resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Bot bot) {
|
||||
Model model = resolveModel(bot.getModelId());
|
||||
BotCategory category = resolveCategory(bot.getCategoryId());
|
||||
SysDept dept = resolveDept(bot.getDeptId());
|
||||
@@ -228,13 +185,59 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Bot update = new Bot();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
botService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "聊天助手";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRemove(BigInteger resourceId) {
|
||||
removeBotRelations(resourceId);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildWorkflowBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId);
|
||||
List<BotWorkflow> relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotWorkflow::getWorkflowId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotWorkflow relation : relations) {
|
||||
Workflow workflow = relation.getWorkflow();
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isSelectableForBot()) {
|
||||
throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
@@ -248,10 +251,11 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildKnowledgeBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId);
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotDocumentCollection::getDocumentCollectionId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotDocumentCollection relation : relations) {
|
||||
DocumentCollection knowledge = relation.getKnowledge();
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isSelectableForBot()) {
|
||||
throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
@@ -266,6 +270,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildPluginBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId);
|
||||
List<BotPlugin> relations = botPluginService.list(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotPlugin::getPluginItemId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotPlugin relation : relations) {
|
||||
PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId());
|
||||
@@ -274,7 +279,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("pluginItemId", relation.getPluginItemId());
|
||||
item.put("pluginItemName", resolvePluginName(pluginItem));
|
||||
item.put("pluginItemName", pluginItem.getName());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
@@ -283,156 +288,85 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
private List<Map<String, Object>> buildMcpBindings(BigInteger botId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId);
|
||||
List<BotMcp> relations = botMcpService.list(queryWrapper);
|
||||
relations.sort(Comparator.comparing(BotMcp::getMcpId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (BotMcp relation : relations) {
|
||||
Mcp mcp = mcpService.getById(relation.getMcpId());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("聊天助手绑定的MCP不存在,无法发布聊天助手");
|
||||
throw new BusinessException("聊天助手绑定的 MCP 不存在,无法发布聊天助手");
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("mcpId", relation.getMcpId());
|
||||
item.put("mcpName", mcp.getTitle());
|
||||
item.put("mcpToolName", relation.getMcpToolName());
|
||||
item.put("mcpToolDescription", relation.getMcpToolDescription());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天助手分类。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 分类实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private BotCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
}
|
||||
return botCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门信息。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 部门实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
}
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @return 模型实体
|
||||
*/
|
||||
private Model resolveModel(BigInteger modelId) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
||||
}
|
||||
Model model = modelService.getById(modelId);
|
||||
if (model == null) {
|
||||
throw new BusinessException("聊天助手绑定的模型不存在,无法提交审批");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模型展示名称。
|
||||
*
|
||||
* @param model 模型实体
|
||||
* @return 模型名称
|
||||
*/
|
||||
private String resolveModelName(Model model) {
|
||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
||||
return model.getTitle();
|
||||
}
|
||||
return model.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取系统提示词。
|
||||
*
|
||||
* @param bot 聊天助手
|
||||
* @return 系统提示词
|
||||
*/
|
||||
private String resolveSystemPrompt(Bot bot) {
|
||||
if (bot.getModelOptions() == null) {
|
||||
return null;
|
||||
}
|
||||
Object prompt = bot.getModelOptions().get(Bot.KEY_SYSTEM_PROMPT);
|
||||
return prompt == null ? null : String.valueOf(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数值配置项。
|
||||
*
|
||||
* @param options 配置 map
|
||||
* @param key 配置键
|
||||
* @return 数值配置,不存在时返回 {@code null}
|
||||
*/
|
||||
private Number readNumberOption(Map<String, Object> options, String key) {
|
||||
if (options == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = options.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成插件工具展示名称。
|
||||
*
|
||||
* @param pluginItem 插件工具实体
|
||||
* @return 展示名称
|
||||
*/
|
||||
private String resolvePluginName(PluginItem pluginItem) {
|
||||
if (pluginItem.getName() != null && !pluginItem.getName().isBlank()) {
|
||||
return pluginItem.getName();
|
||||
}
|
||||
return pluginItem.getEnglishName();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少聊天助手发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger botId) {
|
||||
Bot bot = botService.getById(botId);
|
||||
if (bot == null) {
|
||||
return;
|
||||
}
|
||||
Bot update = new Bot();
|
||||
update.setId(botId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
private void removeBotRelations(BigInteger botId) {
|
||||
botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
|
||||
botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
|
||||
botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
|
||||
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
|
||||
}
|
||||
|
||||
private BotCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
}
|
||||
return botCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
}
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
private Model resolveModel(BigInteger modelId) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
||||
}
|
||||
Model model = modelService.getById(modelId);
|
||||
if (model == null) {
|
||||
throw new BusinessException("聊天助手关联模型不存在,无法提交审批");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private String resolveModelName(Model model) {
|
||||
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||
if (providerName == null || providerName.isBlank()) {
|
||||
return model.getModelName();
|
||||
}
|
||||
return providerName + " / " + model.getModelName();
|
||||
}
|
||||
|
||||
private String resolveSystemPrompt(Bot bot) {
|
||||
if (bot.getModelOptions() == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = bot.getModelOptions().get("systemPrompt");
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Number readNumberOption(Map<String, Object> options, String key) {
|
||||
if (options == null || !options.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
Object value = options.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number;
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Double.valueOf(string);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,36 +15,58 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class BotPublishAppService {
|
||||
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public BotPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手发布审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "BOT",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手下线审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手删除审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 助手 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "BOT",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
@@ -12,43 +12,36 @@ import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库审批处理器。
|
||||
* 知识库生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<DocumentCollection> {
|
||||
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||
private final ModelService modelService;
|
||||
private final DocumentCollectionCategoryService documentCollectionCategoryService;
|
||||
private final SysDeptService sysDeptService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
|
||||
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
@@ -56,14 +49,17 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
BotDocumentCollectionService botDocumentCollectionService,
|
||||
ModelService modelService,
|
||||
DocumentCollectionCategoryService documentCollectionCategoryService,
|
||||
SysDeptService sysDeptService) {
|
||||
SysDeptService sysDeptService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
this.modelService = modelService;
|
||||
this.documentCollectionCategoryService = documentCollectionCategoryService;
|
||||
this.sysDeptService = sysDeptService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,72 +67,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.KNOWLEDGE.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
DocumentCollection knowledge = requireKnowledge(resourceId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前知识库存在未结束审批,请先处理完成");
|
||||
}
|
||||
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(knowledge);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(knowledge.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前知识库没有变更,无需重复发布");
|
||||
}
|
||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(knowledge.getCategoryId());
|
||||
request.setDeptId(knowledge.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "知识库:" + knowledge.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
documentCollectionService.updateById(update);
|
||||
return;
|
||||
}
|
||||
documentCollectionService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
||||
@@ -146,20 +76,47 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentCollection requireKnowledge(BigInteger id) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(id);
|
||||
@Override
|
||||
protected DocumentCollection requireResource(BigInteger resourceId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(resourceId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger knowledgeId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
||||
return botDocumentCollectionService.exists(queryWrapper);
|
||||
@Override
|
||||
protected void assertManagePermission(DocumentCollection resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, resource, ResourceAction.MANAGE, "无权限管理知识库");
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
||||
@Override
|
||||
protected BigInteger getCategoryId(DocumentCollection resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(DocumentCollection resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(DocumentCollection resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(DocumentCollection resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(DocumentCollection resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
||||
Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批");
|
||||
Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批");
|
||||
DocumentCollectionCategory category = resolveCategory(collection.getCategoryId());
|
||||
@@ -197,12 +154,82 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库分类信息。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 分类实体,不存在时返回 {@code null}
|
||||
*/
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
documentCollectionService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "知识库";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enrichOfflineSnapshot(DocumentCollection resource, Map<String, Object> snapshot) {
|
||||
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkKnowledgeImpact(resource.getId());
|
||||
if (!impact.isCanProceed()) {
|
||||
throw new BusinessException(buildWorkflowUsageBlockMessage(impact));
|
||||
}
|
||||
if (impact.isHasBotBindings()) {
|
||||
snapshot.put("botBindings", impact.getBotBindings());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateDelete(DocumentCollection resource, PublishStatus currentStatus) {
|
||||
if (hasBotBinding(resource.getId())) {
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
resourceOfflineImpactService.unbindKnowledgeFromBots(resourceId);
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger knowledgeId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
||||
return botDocumentCollectionService.exists(queryWrapper);
|
||||
}
|
||||
|
||||
private String buildWorkflowUsageBlockMessage(OfflineImpactCheckVo impact) {
|
||||
String names = impact.getWorkflowUsages().stream()
|
||||
.map(item -> item.getTitle() == null ? String.valueOf(item.getId()) : item.getTitle())
|
||||
.reduce((left, right) -> left + "、" + right)
|
||||
.orElse("未知工作流");
|
||||
return "当前知识库被以下工作流使用:" + names + ",请先在工作流中调整解绑后再下线";
|
||||
}
|
||||
|
||||
private DocumentCollectionCategory resolveCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return null;
|
||||
@@ -210,12 +237,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return documentCollectionCategoryService.getById(categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门信息。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 部门实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private SysDept resolveDept(BigInteger deptId) {
|
||||
if (deptId == null) {
|
||||
return null;
|
||||
@@ -223,13 +244,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return sysDeptService.getById(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析必填模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @param errorMessage 模型不存在时抛出的提示
|
||||
* @return 模型实体
|
||||
*/
|
||||
private Model resolveModel(BigInteger modelId, String errorMessage) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException(errorMessage);
|
||||
@@ -241,13 +255,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析可选模型。
|
||||
*
|
||||
* @param modelId 模型 ID
|
||||
* @param errorMessage 模型不存在时抛出的提示
|
||||
* @return 模型实体,不存在时返回 {@code null}
|
||||
*/
|
||||
private Model resolveOptionalModel(BigInteger modelId, String errorMessage) {
|
||||
if (modelId == null) {
|
||||
return null;
|
||||
@@ -259,75 +266,30 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模型展示名称。
|
||||
*
|
||||
* @param model 模型实体
|
||||
* @return 模型名称
|
||||
*/
|
||||
private String resolveModelName(Model model) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
||||
return model.getTitle();
|
||||
}
|
||||
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||
if (providerName == null || providerName.isBlank()) {
|
||||
return model.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库可见范围文案。
|
||||
*
|
||||
* @param visibilityScope 可见范围编码
|
||||
* @return 展示文案
|
||||
*/
|
||||
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
||||
return switch (VisibilityScope.fromOrDefault(visibilityScope, VisibilityScope.PRIVATE)) {
|
||||
case DEPT -> "部门";
|
||||
case PUBLIC -> "公开";
|
||||
default -> "个人";
|
||||
};
|
||||
return providerName + " / " + model.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析知识库类型文案。
|
||||
*
|
||||
* @param collectionType 知识库类型
|
||||
* @return 展示文案
|
||||
*/
|
||||
private String resolveCollectionTypeLabel(String collectionType) {
|
||||
if (DocumentCollection.TYPE_FAQ.equalsIgnoreCase(collectionType)) {
|
||||
if ("FAQ".equalsIgnoreCase(collectionType)) {
|
||||
return "FAQ";
|
||||
}
|
||||
return "文档";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少知识库发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger knowledgeId) {
|
||||
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(knowledgeId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
documentCollectionService.updateById(update);
|
||||
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
||||
VisibilityScope scope = VisibilityScope.from(visibilityScope);
|
||||
return switch (scope) {
|
||||
case PRIVATE -> "仅自己";
|
||||
case DEPT -> "本部门";
|
||||
case PUBLIC -> "公开";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,36 +20,82 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class KnowledgePublishAppService {
|
||||
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public KnowledgePublishAppService(DocumentCollectionService documentCollectionService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库发布审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "KNOWLEDGE",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库下线审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查知识库下线影响。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||
assertId(id);
|
||||
DocumentCollection knowledge = documentCollectionService.getById(id);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前知识库尚未发布,无法下线");
|
||||
}
|
||||
return resourceOfflineImpactService.checkKnowledgeImpact(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交知识库删除审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 知识库 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "KNOWLEDGE",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id);
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id) {
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流审批处理器。
|
||||
* 工作流生命周期处理器。
|
||||
*/
|
||||
@Component
|
||||
public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
|
||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||
public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Workflow> {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
|
||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
ApprovalInstanceService approvalInstanceService,
|
||||
BotWorkflowService botWorkflowService) {
|
||||
BotWorkflowService botWorkflowService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,72 +50,6 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
return ApprovalResourceType.WORKFLOW.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
Workflow workflow = requireWorkflow(resourceId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||
throw new BusinessException("当前工作流存在未结束审批,请先处理完成");
|
||||
}
|
||||
|
||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(workflow);
|
||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(workflow.getPublishedSnapshotJson()))) {
|
||||
throw new BusinessException("当前工作流没有变更,无需重复发布");
|
||||
}
|
||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
||||
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
||||
}
|
||||
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setResourceType(resourceType());
|
||||
request.setResourceId(resourceId);
|
||||
request.setActionType(approvalActionType.getCode());
|
||||
request.setApplicantId(operatorId);
|
||||
request.setCategoryId(workflow.getCategoryId());
|
||||
request.setDeptId(workflow.getDeptId());
|
||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "工作流:" + workflow.getTitle());
|
||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(context.getResourceId());
|
||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext context) {
|
||||
ApprovalInstance instance = context.getInstance();
|
||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(instance.getResourceId());
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
||||
update.setPublishedAt(new Date());
|
||||
update.setPublishedBy(context.getOperatorId());
|
||||
workflowService.updateById(update);
|
||||
return;
|
||||
}
|
||||
workflowService.removeById(instance.getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoked(ApprovalCallbackContext context) {
|
||||
clearPendingStatus(context.getInstance().getResourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
||||
@@ -129,62 +59,125 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private Workflow requireWorkflow(BigInteger id) {
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
@Override
|
||||
protected Workflow requireResource(BigInteger resourceId) {
|
||||
Workflow workflow = workflowService.getById(resourceId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertManagePermission(Workflow resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, resource, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Workflow resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Workflow resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Workflow resource) {
|
||||
return resource.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Workflow resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Workflow resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Workflow resource) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", resource.getId());
|
||||
snapshot.put("alias", resource.getAlias());
|
||||
snapshot.put("deptId", resource.getDeptId());
|
||||
snapshot.put("tenantId", resource.getTenantId());
|
||||
snapshot.put("title", resource.getTitle());
|
||||
snapshot.put("description", resource.getDescription());
|
||||
snapshot.put("icon", resource.getIcon());
|
||||
snapshot.put("content", resource.getContent());
|
||||
snapshot.put("englishName", resource.getEnglishName());
|
||||
snapshot.put("status", resource.getStatus());
|
||||
snapshot.put("categoryId", resource.getCategoryId());
|
||||
snapshot.put("visibilityScope", resource.getVisibilityScope());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(publishStatus.getCode());
|
||||
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Workflow update = new Workflow();
|
||||
update.setId(resourceId);
|
||||
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
workflowService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "工作流";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enrichOfflineSnapshot(Workflow resource, Map<String, Object> snapshot) {
|
||||
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
|
||||
if (impact.isHasBotBindings()) {
|
||||
snapshot.put("botBindings", impact.getBotBindings());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateDelete(Workflow resource, PublishStatus currentStatus) {
|
||||
if (hasBotBinding(resource.getId())) {
|
||||
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterOffline(BigInteger resourceId) {
|
||||
resourceOfflineImpactService.unbindWorkflowFromBots(resourceId);
|
||||
}
|
||||
|
||||
private boolean hasBotBinding(BigInteger workflowId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
|
||||
return botWorkflowService.exists(queryWrapper);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResourceSnapshot(Workflow workflow) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", workflow.getId());
|
||||
snapshot.put("alias", workflow.getAlias());
|
||||
snapshot.put("deptId", workflow.getDeptId());
|
||||
snapshot.put("tenantId", workflow.getTenantId());
|
||||
snapshot.put("title", workflow.getTitle());
|
||||
snapshot.put("description", workflow.getDescription());
|
||||
snapshot.put("icon", workflow.getIcon());
|
||||
snapshot.put("content", workflow.getContent());
|
||||
snapshot.put("englishName", workflow.getEnglishName());
|
||||
snapshot.put("status", workflow.getStatus());
|
||||
snapshot.put("categoryId", workflow.getCategoryId());
|
||||
snapshot.put("visibilityScope", workflow.getVisibilityScope());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||
throw new BusinessException("审批快照缺少工作流发布内容");
|
||||
}
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
|
||||
private PublishStatus resolvePendingStatus(String actionType) {
|
||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
||||
? PublishStatus.DELETE_PENDING
|
||||
: PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
|
||||
private void clearPendingStatus(BigInteger workflowId) {
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
return;
|
||||
}
|
||||
Workflow update = new Workflow();
|
||||
update.setId(workflowId);
|
||||
update.setCurrentApprovalInstanceId(null);
|
||||
update.setPublishStatus(workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()
|
||||
? PublishStatus.DRAFT.getCode()
|
||||
: PublishStatus.PUBLISHED.getCode());
|
||||
workflowService.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package tech.easyflow.ai.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -14,36 +20,82 @@ import java.math.BigInteger;
|
||||
@Service
|
||||
public class WorkflowPublishAppService {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
public WorkflowPublishAppService(WorkflowService workflowService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.workflowService = workflowService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流发布审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "WORKFLOW",
|
||||
actionType = "PUBLISH",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流下线审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.OFFLINE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工作流下线影响。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
if (PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("当前工作流尚未发布,无法下线");
|
||||
}
|
||||
return resourceOfflineImpactService.checkWorkflowImpact(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工作流删除审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流 ID
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
@ApprovalAction(
|
||||
resourceType = "WORKFLOW",
|
||||
actionType = "DELETE",
|
||||
idExpr = "#id"
|
||||
)
|
||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode());
|
||||
return id;
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.DELETE.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertId(BigInteger id, String resourceType, String actionType) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* AI 资源审批状态派生服务。
|
||||
* <p>
|
||||
* 该服务仅负责根据当前审批实例与资源真实状态派生只读展示字段,
|
||||
* 不参与资源状态回写,也不在读链路中执行任何数据库修复操作。
|
||||
*/
|
||||
public interface AiResourceApprovalStateService {
|
||||
|
||||
/**
|
||||
* 填充工作流审批展示状态。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
*/
|
||||
void fillWorkflowApprovalState(Workflow workflow);
|
||||
|
||||
/**
|
||||
* 批量填充工作流审批展示状态。
|
||||
*
|
||||
* @param workflows 工作流集合
|
||||
*/
|
||||
void fillWorkflowApprovalState(Collection<Workflow> workflows);
|
||||
|
||||
/**
|
||||
* 填充知识库审批展示状态。
|
||||
*
|
||||
* @param collection 知识库
|
||||
*/
|
||||
void fillKnowledgeApprovalState(DocumentCollection collection);
|
||||
|
||||
/**
|
||||
* 批量填充知识库审批展示状态。
|
||||
*
|
||||
* @param collections 知识库集合
|
||||
*/
|
||||
void fillKnowledgeApprovalState(Collection<DocumentCollection> collections);
|
||||
|
||||
/**
|
||||
* 填充聊天助手审批展示状态。
|
||||
*
|
||||
* @param bot 聊天助手
|
||||
*/
|
||||
void fillBotApprovalState(Bot bot);
|
||||
|
||||
/**
|
||||
* 批量填充聊天助手审批展示状态。
|
||||
*
|
||||
* @param bots 聊天助手集合
|
||||
*/
|
||||
void fillBotApprovalState(Collection<Bot> bots);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 资源下线影响检查与解绑服务。
|
||||
*/
|
||||
public interface ResourceOfflineImpactService {
|
||||
|
||||
/**
|
||||
* 检查工作流下线影响。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId);
|
||||
|
||||
/**
|
||||
* 检查知识库下线影响。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 下线影响结果
|
||||
*/
|
||||
OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId);
|
||||
|
||||
/**
|
||||
* 工作流下线后,静默解绑所有关联 Bot。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
*/
|
||||
void unbindWorkflowFromBots(BigInteger workflowId);
|
||||
|
||||
/**
|
||||
* 知识库下线后,静默解绑所有关联 Bot。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
*/
|
||||
void unbindKnowledgeFromBots(BigInteger knowledgeId);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI 资源审批状态派生服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalStateService {
|
||||
|
||||
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
public AiResourceApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillWorkflowApprovalState(Workflow workflow) {
|
||||
fillWorkflowApprovalState(workflow == null ? List.of() : List.of(workflow));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillWorkflowApprovalState(Collection<Workflow> workflows) {
|
||||
fillApprovalState(
|
||||
workflows,
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
Workflow::getCurrentApprovalInstanceId,
|
||||
workflow -> PublishStatus.from(workflow.getPublishStatus()),
|
||||
Workflow::getPublishedSnapshotJson,
|
||||
Workflow::setApprovalPending,
|
||||
Workflow::setCurrentApprovalActionType,
|
||||
Workflow::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillKnowledgeApprovalState(DocumentCollection collection) {
|
||||
fillKnowledgeApprovalState(collection == null ? List.of() : List.of(collection));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillKnowledgeApprovalState(Collection<DocumentCollection> collections) {
|
||||
fillApprovalState(
|
||||
collections,
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
DocumentCollection::getCurrentApprovalInstanceId,
|
||||
collection -> PublishStatus.from(collection.getPublishStatus()),
|
||||
DocumentCollection::getPublishedSnapshotJson,
|
||||
DocumentCollection::setApprovalPending,
|
||||
DocumentCollection::setCurrentApprovalActionType,
|
||||
DocumentCollection::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillBotApprovalState(Bot bot) {
|
||||
fillBotApprovalState(bot == null ? List.of() : List.of(bot));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillBotApprovalState(Collection<Bot> bots) {
|
||||
fillApprovalState(
|
||||
bots,
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
Bot::getCurrentApprovalInstanceId,
|
||||
bot -> PublishStatus.from(bot.getPublishStatus()),
|
||||
Bot::getPublishedSnapshotJson,
|
||||
Bot::setApprovalPending,
|
||||
Bot::setCurrentApprovalActionType,
|
||||
Bot::setDisplayPublishStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一派生审批展示状态。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param resourceType 资源类型
|
||||
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||
* @param statusGetter 发布状态获取器
|
||||
* @param snapshotGetter 已发布快照获取器
|
||||
* @param pendingSetter 审批中标记写入器
|
||||
* @param actionSetter 当前审批动作写入器
|
||||
* @param displaySetter 展示状态写入器
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
private <T> void fillApprovalState(Collection<T> resources,
|
||||
String resourceType,
|
||||
Function<T, BigInteger> instanceIdGetter,
|
||||
Function<T, PublishStatus> statusGetter,
|
||||
Function<T, Map<String, Object>> snapshotGetter,
|
||||
BiConsumer<T, Boolean> pendingSetter,
|
||||
BiConsumer<T, String> actionSetter,
|
||||
BiConsumer<T, String> displaySetter) {
|
||||
if (CollectionUtils.isEmpty(resources)) {
|
||||
return;
|
||||
}
|
||||
List<T> validResources = resources.stream().filter(Objects::nonNull).toList();
|
||||
if (validResources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validResources, instanceIdGetter);
|
||||
for (T resource : validResources) {
|
||||
PublishStatus currentStatus = statusGetter.apply(resource);
|
||||
BigInteger instanceId = instanceIdGetter.apply(resource);
|
||||
ApprovalInstance instance = instanceId == null ? null : instanceMap.get(instanceId);
|
||||
if (!isValidCurrentInstance(resourceType, instance)) {
|
||||
pendingSetter.accept(resource, false);
|
||||
actionSetter.accept(resource, null);
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||
if (instanceStatus.isFinished()) {
|
||||
pendingSetter.accept(resource, false);
|
||||
actionSetter.accept(resource, null);
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||
pendingSetter.accept(resource, true);
|
||||
actionSetter.accept(resource, actionType.getCode());
|
||||
displaySetter.accept(resource, resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载当前审批实例。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||
* @param <T> 资源类型
|
||||
* @return 审批实例映射
|
||||
*/
|
||||
private <T> Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<T> resources,
|
||||
Function<T, BigInteger> instanceIdGetter) {
|
||||
Set<BigInteger> instanceIds = resources.stream()
|
||||
.map(instanceIdGetter)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (instanceIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||
);
|
||||
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断审批实例是否仍是当前资源的有效实例。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param instance 审批实例
|
||||
* @return 有效返回 true
|
||||
*/
|
||||
private boolean isValidCurrentInstance(String resourceType, ApprovalInstance instance) {
|
||||
return instance != null && resourceType.equals(instance.getResourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在没有活动审批实例时派生展示状态。
|
||||
*
|
||||
* @param currentStatus 当前真实状态
|
||||
* @param publishedSnapshot 已发布快照
|
||||
* @return 展示状态
|
||||
*/
|
||||
private PublishStatus resolveDisplayStatusWithoutActiveInstance(PublishStatus currentStatus,
|
||||
Map<String, Object> publishedSnapshot) {
|
||||
// 读接口不再猜测或掩盖真实状态。若资源表已经落成 pending 但当前找不到有效审批实例,
|
||||
// 页面应直接看到真实状态,后续再由独立修复流程处理脏数据,而不是在展示层伪装成已发布。
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在存在活动审批实例时派生展示状态。
|
||||
*
|
||||
* @param currentStatus 当前真实状态
|
||||
* @param actionType 当前审批动作
|
||||
* @return 展示状态
|
||||
*/
|
||||
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||
ApprovalActionType actionType) {
|
||||
if (currentStatus == PublishStatus.PUBLISHED && actionType == ApprovalActionType.PUBLISH) {
|
||||
return PublishStatus.PUBLISH_PENDING;
|
||||
}
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
return currentStatus;
|
||||
}
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 资源下线影响检查与 Bot 静默解绑实现。
|
||||
*/
|
||||
@Service
|
||||
public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactService {
|
||||
|
||||
private static final String KNOWLEDGE_NODE_TYPE = "knowledgeNode";
|
||||
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
|
||||
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||
private final BotService botService;
|
||||
private final WorkflowService workflowService;
|
||||
private final RedisLockExecutor redisLockExecutor;
|
||||
|
||||
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService,
|
||||
BotService botService,
|
||||
WorkflowService workflowService,
|
||||
RedisLockExecutor redisLockExecutor) {
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||
this.botService = botService;
|
||||
this.workflowService = workflowService;
|
||||
this.redisLockExecutor = redisLockExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
|
||||
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
|
||||
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||
result.setCanProceed(true);
|
||||
result.setBotBindings(botBindings);
|
||||
result.setHasBotBindings(!botBindings.isEmpty());
|
||||
result.setWorkflowUsages(Collections.emptyList());
|
||||
result.setHasWorkflowUsages(false);
|
||||
result.setMessage(botBindings.isEmpty()
|
||||
? "当前工作流下线后不会影响已有绑定"
|
||||
: "当前工作流下线成功后,将自动从相关聊天助手中解绑");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId) {
|
||||
List<OfflineImpactBindingVo> botBindings = listBotsByKnowledgeId(knowledgeId);
|
||||
List<OfflineImpactBindingVo> workflowUsages = listWorkflowsUsingKnowledge(knowledgeId);
|
||||
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||
result.setBotBindings(botBindings);
|
||||
result.setHasBotBindings(!botBindings.isEmpty());
|
||||
result.setWorkflowUsages(workflowUsages);
|
||||
result.setHasWorkflowUsages(!workflowUsages.isEmpty());
|
||||
result.setCanProceed(workflowUsages.isEmpty());
|
||||
result.setMessage(workflowUsages.isEmpty()
|
||||
? (botBindings.isEmpty()
|
||||
? "当前知识库下线后不会影响已有绑定"
|
||||
: "当前知识库下线成功后,将自动从相关聊天助手中解绑")
|
||||
: "当前知识库仍被工作流使用,请先调整工作流后再下线");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindWorkflowFromBots(BigInteger workflowId) {
|
||||
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
Set<BigInteger> botIds = collectBotIds(relations, BotWorkflow::getBotId);
|
||||
for (BigInteger botId : botIds) {
|
||||
redisLockExecutor.executeWithLock(
|
||||
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
botWorkflowService.remove(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getBotId, botId)
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
trimPublishedSnapshotBindings(botId, "workflowBindings", "workflowId", workflowId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindKnowledgeFromBots(BigInteger knowledgeId) {
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
Set<BigInteger> botIds = collectBotIds(relations, BotDocumentCollection::getBotId);
|
||||
for (BigInteger botId : botIds) {
|
||||
redisLockExecutor.executeWithLock(
|
||||
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
botDocumentCollectionService.remove(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getBotId, botId)
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
trimPublishedSnapshotBindings(botId, "knowledgeBindings", "knowledgeId", knowledgeId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByWorkflowId(BigInteger workflowId) {
|
||||
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||
return listBotsByIds(collectBotIds(relations, BotWorkflow::getBotId));
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByKnowledgeId(BigInteger knowledgeId) {
|
||||
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||
return listBotsByIds(collectBotIds(relations, BotDocumentCollection::getBotId));
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listBotsByIds(Set<BigInteger> botIds) {
|
||||
if (botIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Bot> bots = botService.listByIds(botIds);
|
||||
Map<BigInteger, Bot> botMap = new HashMap<>();
|
||||
for (Bot bot : bots) {
|
||||
botMap.put(bot.getId(), bot);
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>(botIds.size());
|
||||
for (BigInteger botId : botIds) {
|
||||
Bot bot = botMap.get(botId);
|
||||
if (bot == null) {
|
||||
continue;
|
||||
}
|
||||
result.add(toBindingVo(bot.getId(), bot.getTitle()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<OfflineImpactBindingVo> listWorkflowsUsingKnowledge(BigInteger knowledgeId) {
|
||||
List<Workflow> workflows = workflowService.list();
|
||||
if (workflows == null || workflows.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>();
|
||||
for (Workflow workflow : workflows) {
|
||||
if (workflow == null || workflow.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (containsKnowledgeReference(workflow.getContent(), knowledgeId)) {
|
||||
result.add(toBindingVo(workflow.getId(), workflow.getTitle()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
|
||||
if (!StringUtils.hasText(content) || knowledgeId == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JSON.parse(content);
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
return false;
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String expected = knowledgeId.toString();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null || !KNOWLEDGE_NODE_TYPE.equals(node.getString("type"))) {
|
||||
continue;
|
||||
}
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
Object rawKnowledgeId = data.get("knowledgeId");
|
||||
if (rawKnowledgeId != null && expected.equals(String.valueOf(rawKnowledgeId))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void trimPublishedSnapshotBindings(BigInteger botId,
|
||||
String bindingsKey,
|
||||
String idKey,
|
||||
BigInteger resourceId) {
|
||||
Bot bot = botService.getById(botId);
|
||||
if (bot == null || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>(bot.getPublishedSnapshotJson());
|
||||
Object rawBindings = snapshot.get(bindingsKey);
|
||||
if (!(rawBindings instanceof List<?> bindings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> filtered = new ArrayList<>();
|
||||
boolean changed = false;
|
||||
String expectedId = resourceId == null ? null : resourceId.toString();
|
||||
for (Object item : bindings) {
|
||||
if (!(item instanceof Map<?, ?> bindingMap)) {
|
||||
continue;
|
||||
}
|
||||
Object currentId = bindingMap.get(idKey);
|
||||
if (expectedId != null && currentId != null && expectedId.equals(String.valueOf(currentId))) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
filtered.add(new LinkedHashMap<>((Map<String, Object>) bindingMap));
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
snapshot.put(bindingsKey, filtered);
|
||||
Bot update = new Bot();
|
||||
update.setId(botId);
|
||||
update.setPublishedSnapshotJson(snapshot);
|
||||
botService.updateById(update);
|
||||
}
|
||||
|
||||
private <T> Set<BigInteger> collectBotIds(Collection<T> relations, BotIdGetter<T> getter) {
|
||||
if (relations == null || relations.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
for (T relation : relations) {
|
||||
BigInteger botId = getter.getBotId(relation);
|
||||
if (botId != null) {
|
||||
result.add(botId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private OfflineImpactBindingVo toBindingVo(BigInteger id, String title) {
|
||||
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
|
||||
vo.setId(id);
|
||||
vo.setTitle(title);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface BotIdGetter<T> {
|
||||
|
||||
BigInteger getBotId(T relation);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.poi.extractor.ExtractorFactory;
|
||||
import org.apache.poi.extractor.POITextExtractor;
|
||||
import org.apache.pdfbox.multipdf.Splitter;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
@@ -22,8 +24,10 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class DocUtil {
|
||||
@@ -85,6 +89,27 @@ public class DocUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取素材预览文本。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @param is 文件输入流
|
||||
* @return 预览文本
|
||||
*/
|
||||
public static String readPreviewContent(String suffix, InputStream is) {
|
||||
String normalizedSuffix = normalizeSuffix(suffix);
|
||||
if (isPlainTextSuffix(normalizedSuffix)) {
|
||||
return readPlainTextFile(is);
|
||||
}
|
||||
if ("pdf".equals(normalizedSuffix)) {
|
||||
return readPdfFile(is);
|
||||
}
|
||||
if (isOfficeSuffix(normalizedSuffix)) {
|
||||
return readOfficeFile(is);
|
||||
}
|
||||
throw new IllegalArgumentException("不支持的文件类型: " + suffix);
|
||||
}
|
||||
|
||||
public static Map<Integer, byte[]> splitPdf(byte[] bytes, int splitSize) {
|
||||
|
||||
Map<Integer, byte[]> map = new HashMap<>();
|
||||
@@ -174,6 +199,16 @@ public class DocUtil {
|
||||
return name.substring(name.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化文件后缀,统一使用小写。
|
||||
*
|
||||
* @param suffix 原始文件后缀
|
||||
* @return 规范化后的后缀
|
||||
*/
|
||||
public static String normalizeSuffix(String suffix) {
|
||||
return suffix == null ? "" : suffix.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public static byte[] readBytes(InputStream inputStream) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
try {
|
||||
@@ -194,4 +229,59 @@ public class DocUtil {
|
||||
public static String getFileNameByUrl(String url) {
|
||||
return url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取纯文本类型文件。
|
||||
*
|
||||
* @param is 文件输入流
|
||||
* @return 文本内容
|
||||
*/
|
||||
private static String readPlainTextFile(InputStream is) {
|
||||
try {
|
||||
return new String(readBytes(is), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("读取文本文件失败:", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Apache POI 提取 Office 文档中的纯文本。
|
||||
*
|
||||
* @param is 文件输入流
|
||||
* @return 文本内容
|
||||
*/
|
||||
private static String readOfficeFile(InputStream is) {
|
||||
try (POITextExtractor extractor = ExtractorFactory.createExtractor(is)) {
|
||||
return extractor.getText();
|
||||
} catch (Exception e) {
|
||||
log.error("读取 Office 文件失败:", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为纯文本类文件。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否纯文本类文件
|
||||
*/
|
||||
private static boolean isPlainTextSuffix(String suffix) {
|
||||
return "txt".equals(suffix) || "md".equals(suffix) || "csv".equals(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Office 文件。
|
||||
*
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否 Office 文件
|
||||
*/
|
||||
private static boolean isOfficeSuffix(String suffix) {
|
||||
return "doc".equals(suffix)
|
||||
|| "docx".equals(suffix)
|
||||
|| "xls".equals(suffix)
|
||||
|| "xlsx".equals(suffix)
|
||||
|| "ppt".equals(suffix)
|
||||
|| "pptx".equals(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 下线影响项视图对象。
|
||||
*/
|
||||
public class OfflineImpactBindingVo {
|
||||
|
||||
private BigInteger id;
|
||||
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 获取资源 ID。
|
||||
*
|
||||
* @return 资源 ID
|
||||
*/
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置资源 ID。
|
||||
*
|
||||
* @param id 资源 ID
|
||||
*/
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源名称。
|
||||
*
|
||||
* @return 资源名称
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置资源名称。
|
||||
*
|
||||
* @param title 资源名称
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 下线影响检查结果。
|
||||
*/
|
||||
public class OfflineImpactCheckVo {
|
||||
|
||||
private boolean canProceed;
|
||||
|
||||
private boolean hasBotBindings;
|
||||
|
||||
private boolean hasWorkflowUsages;
|
||||
|
||||
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
|
||||
|
||||
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
|
||||
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 是否允许继续下线。
|
||||
*
|
||||
* @return 是否允许继续
|
||||
*/
|
||||
public boolean isCanProceed() {
|
||||
return canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否允许继续下线。
|
||||
*
|
||||
* @param canProceed 是否允许继续
|
||||
*/
|
||||
public void setCanProceed(boolean canProceed) {
|
||||
this.canProceed = canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在 Bot 绑定。
|
||||
*
|
||||
* @return 是否存在 Bot 绑定
|
||||
*/
|
||||
public boolean isHasBotBindings() {
|
||||
return hasBotBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否存在 Bot 绑定。
|
||||
*
|
||||
* @param hasBotBindings 是否存在 Bot 绑定
|
||||
*/
|
||||
public void setHasBotBindings(boolean hasBotBindings) {
|
||||
this.hasBotBindings = hasBotBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在工作流引用。
|
||||
*
|
||||
* @return 是否存在工作流引用
|
||||
*/
|
||||
public boolean isHasWorkflowUsages() {
|
||||
return hasWorkflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否存在工作流引用。
|
||||
*
|
||||
* @param hasWorkflowUsages 是否存在工作流引用
|
||||
*/
|
||||
public void setHasWorkflowUsages(boolean hasWorkflowUsages) {
|
||||
this.hasWorkflowUsages = hasWorkflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Bot 绑定列表。
|
||||
*
|
||||
* @return Bot 绑定列表
|
||||
*/
|
||||
public List<OfflineImpactBindingVo> getBotBindings() {
|
||||
return botBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Bot 绑定列表。
|
||||
*
|
||||
* @param botBindings Bot 绑定列表
|
||||
*/
|
||||
public void setBotBindings(List<OfflineImpactBindingVo> botBindings) {
|
||||
this.botBindings = botBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流引用列表。
|
||||
*
|
||||
* @return 工作流引用列表
|
||||
*/
|
||||
public List<OfflineImpactBindingVo> getWorkflowUsages() {
|
||||
return workflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作流引用列表。
|
||||
*
|
||||
* @param workflowUsages 工作流引用列表
|
||||
*/
|
||||
public void setWorkflowUsages(List<OfflineImpactBindingVo> workflowUsages) {
|
||||
this.workflowUsages = workflowUsages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提示信息。
|
||||
*
|
||||
* @return 提示信息
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提示信息。
|
||||
*
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,15 @@ import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提审动作切面。
|
||||
@@ -25,11 +29,14 @@ import java.math.BigInteger;
|
||||
public class ApprovalActionAspect {
|
||||
|
||||
private final ApprovalActionFacade approvalActionFacade;
|
||||
private final List<ApprovalSubjectHandler> handlers;
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade) {
|
||||
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade,
|
||||
List<ApprovalSubjectHandler> handlers) {
|
||||
this.approvalActionFacade = approvalActionFacade;
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +44,7 @@ public class ApprovalActionAspect {
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param approvalAction 注解
|
||||
* @return 审批实例 ID
|
||||
* @return 动作执行结果
|
||||
* @throws Throwable 执行异常
|
||||
*/
|
||||
@Around("@annotation(approvalAction)")
|
||||
@@ -45,12 +52,14 @@ public class ApprovalActionAspect {
|
||||
Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr());
|
||||
BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier));
|
||||
joinPoint.proceed();
|
||||
return approvalActionFacade.submit(
|
||||
approvalAction.resourceType(),
|
||||
ApprovalSubjectHandler handler = getHandler(approvalAction.resourceType());
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(
|
||||
resourceId,
|
||||
approvalAction.actionType(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
ApprovalActionResult result = approvalActionFacade.submit(request);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) {
|
||||
@@ -63,4 +72,11 @@ public class ApprovalActionAspect {
|
||||
);
|
||||
return expressionParser.parseExpression(idExpr).getValue(context);
|
||||
}
|
||||
|
||||
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||
return handlers.stream()
|
||||
.filter(item -> item.resourceType().equals(resourceType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("未找到审批处理器: " + resourceType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.approval.entity.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 审批动作执行结果。
|
||||
*/
|
||||
public class ApprovalActionResult {
|
||||
|
||||
private boolean approvalRequired;
|
||||
|
||||
private BigInteger instanceId;
|
||||
|
||||
/**
|
||||
* 构造需要审批的结果。
|
||||
*
|
||||
* @param instanceId 审批实例 ID
|
||||
* @return 审批结果
|
||||
*/
|
||||
public static ApprovalActionResult required(BigInteger instanceId) {
|
||||
ApprovalActionResult result = new ApprovalActionResult();
|
||||
result.setApprovalRequired(true);
|
||||
result.setInstanceId(instanceId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造直接执行完成的结果。
|
||||
*
|
||||
* @return 审批结果
|
||||
*/
|
||||
public static ApprovalActionResult direct() {
|
||||
ApprovalActionResult result = new ApprovalActionResult();
|
||||
result.setApprovalRequired(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isApprovalRequired() {
|
||||
return approvalRequired;
|
||||
}
|
||||
|
||||
public void setApprovalRequired(boolean approvalRequired) {
|
||||
this.approvalRequired = approvalRequired;
|
||||
}
|
||||
|
||||
public BigInteger getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public void setInstanceId(BigInteger instanceId) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ public class ApprovalInstanceDetailVo {
|
||||
|
||||
private String applicantName;
|
||||
|
||||
private String applicantAccount;
|
||||
|
||||
private Date submittedAt;
|
||||
|
||||
private Date finishedAt;
|
||||
@@ -137,6 +139,14 @@ public class ApprovalInstanceDetailVo {
|
||||
this.applicantName = applicantName;
|
||||
}
|
||||
|
||||
public String getApplicantAccount() {
|
||||
return applicantAccount;
|
||||
}
|
||||
|
||||
public void setApplicantAccount(String applicantAccount) {
|
||||
this.applicantAccount = applicantAccount;
|
||||
}
|
||||
|
||||
public Date getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class ApprovalLogVo {
|
||||
|
||||
private BigInteger operatorId;
|
||||
|
||||
private String operatorAccount;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private Date created;
|
||||
@@ -45,6 +47,14 @@ public class ApprovalLogVo {
|
||||
this.operatorId = operatorId;
|
||||
}
|
||||
|
||||
public String getOperatorAccount() {
|
||||
return operatorAccount;
|
||||
}
|
||||
|
||||
public void setOperatorAccount(String operatorAccount) {
|
||||
this.operatorAccount = operatorAccount;
|
||||
}
|
||||
|
||||
public String getOperatorName() {
|
||||
return operatorName;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.Locale;
|
||||
public enum ApprovalActionType {
|
||||
|
||||
PUBLISH("PUBLISH"),
|
||||
OFFLINE("OFFLINE"),
|
||||
DELETE("DELETE");
|
||||
|
||||
private final String code;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -12,13 +14,10 @@ public interface ApprovalActionFacade {
|
||||
/**
|
||||
* 提交审批。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 审批实例 ID
|
||||
* @param request 审批提交请求
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
ApprovalActionResult submit(ApprovalSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 处理审批通过后的业务回调。
|
||||
|
||||
@@ -15,4 +15,12 @@ public interface ApprovalMatchService {
|
||||
* @return 命中的流程详情
|
||||
*/
|
||||
ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request);
|
||||
|
||||
/**
|
||||
* 根据资源上下文匹配审批流程,未命中时返回 {@code null}。
|
||||
*
|
||||
* @param request 审批提交请求
|
||||
* @return 命中的流程详情,未命中时返回 {@code null}
|
||||
*/
|
||||
ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 审批结果业务处理器。
|
||||
*/
|
||||
public interface ApprovalResultHandler {
|
||||
|
||||
/**
|
||||
* 处理审批通过后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
|
||||
/**
|
||||
* 处理审批驳回后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
|
||||
/**
|
||||
* 处理审批撤回后的业务回调。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param operatorId 操作人 ID
|
||||
* @param comment 审批意见
|
||||
*/
|
||||
void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package tech.easyflow.approval.service;
|
||||
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -28,34 +26,6 @@ public interface ApprovalSubjectHandler {
|
||||
*/
|
||||
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 审批提交完成后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onSubmitted(ApprovalSubmitCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批通过后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onApproved(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批驳回后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onRejected(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 审批撤回后的回调。
|
||||
*
|
||||
* @param context 回调上下文
|
||||
*/
|
||||
void onRevoked(ApprovalCallbackContext context);
|
||||
|
||||
/**
|
||||
* 校验资源是否已发布。
|
||||
*
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -22,31 +23,30 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
|
||||
private final List<ApprovalSubjectHandler> handlers;
|
||||
private final ApprovalInstanceService approvalInstanceService;
|
||||
private final ApprovalMatchService approvalMatchService;
|
||||
private final ApprovalResultHandler approvalResultHandler;
|
||||
|
||||
public ApprovalActionFacadeImpl(List<ApprovalSubjectHandler> handlers,
|
||||
ApprovalInstanceService approvalInstanceService) {
|
||||
ApprovalInstanceService approvalInstanceService,
|
||||
ApprovalMatchService approvalMatchService,
|
||||
ApprovalResultHandler approvalResultHandler) {
|
||||
this.handlers = handlers;
|
||||
this.approvalInstanceService = approvalInstanceService;
|
||||
this.approvalMatchService = approvalMatchService;
|
||||
this.approvalResultHandler = approvalResultHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
ApprovalSubjectHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
public ApprovalActionResult submit(ApprovalSubmitRequest request) {
|
||||
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||
if (flow == null) {
|
||||
return ApprovalActionResult.direct();
|
||||
}
|
||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||
|
||||
ApprovalSubmitCallbackContext context = new ApprovalSubmitCallbackContext();
|
||||
context.setInstanceId(instanceId);
|
||||
context.setResourceType(request.getResourceType());
|
||||
context.setResourceId(request.getResourceId());
|
||||
context.setActionType(request.getActionType());
|
||||
context.setOperatorId(operatorId);
|
||||
handler.onSubmitted(context);
|
||||
return instanceId;
|
||||
return ApprovalActionResult.required(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +54,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onApproved(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleApproved(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,8 +62,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onRejected(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleRejected(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,8 +70,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
*/
|
||||
@Override
|
||||
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
||||
handler.onRevoked(buildCallbackContext(instance, operatorId, comment));
|
||||
approvalResultHandler.handleRevoked(instance, operatorId, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,14 +82,6 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
||||
handler.assertPublishedAccess(identifier, denyMessage);
|
||||
}
|
||||
|
||||
private ApprovalCallbackContext buildCallbackContext(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||
ApprovalCallbackContext context = new ApprovalCallbackContext();
|
||||
context.setInstance(instance);
|
||||
context.setOperatorId(operatorId);
|
||||
context.setComment(comment);
|
||||
return context;
|
||||
}
|
||||
|
||||
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||
String normalized = ApprovalResourceType.from(resourceType).getCode();
|
||||
return handlers.stream()
|
||||
|
||||
@@ -53,6 +53,18 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
*/
|
||||
@Override
|
||||
public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) {
|
||||
ApprovalFlowDetailVo matchedFlow = matchFlowOrNull(request);
|
||||
if (matchedFlow == null) {
|
||||
throw new BusinessException("当前资源上下文未命中审批流程");
|
||||
}
|
||||
return matchedFlow;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request) {
|
||||
ApprovalSubmitRequest normalized = normalizeRequest(request);
|
||||
QueryWrapper flowWrapper = QueryWrapper.create()
|
||||
.eq(ApprovalFlow::getResourceType, normalized.getResourceType())
|
||||
@@ -60,7 +72,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
.eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode());
|
||||
List<ApprovalFlow> flows = approvalFlowMapper.selectListByQuery(flowWrapper);
|
||||
if (CollectionUtil.isEmpty(flows)) {
|
||||
throw new BusinessException("未找到可用的审批流程");
|
||||
return null;
|
||||
}
|
||||
List<BigInteger> flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList());
|
||||
Map<BigInteger, List<ApprovalFlowScope>> scopeMap = approvalFlowScopeMapper.selectListByQuery(
|
||||
@@ -76,7 +88,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
||||
}
|
||||
}
|
||||
if (matchedFlows.isEmpty()) {
|
||||
throw new BusinessException("当前资源上下文未命中审批流程");
|
||||
return null;
|
||||
}
|
||||
|
||||
matchedFlows.sort(Comparator
|
||||
|
||||
@@ -154,8 +154,9 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
||||
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
||||
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
||||
Map<BigInteger, String> accountNameMap = loadAccountNameMap(instance, tasks, logs);
|
||||
detail.setApplicantName(accountNameMap.get(instance.getApplicantId()));
|
||||
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs);
|
||||
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
|
||||
detail.setApplicantAccount(resolveAccountLoginName(accountMap.get(instance.getApplicantId())));
|
||||
|
||||
detail.setTasks(tasks.stream()
|
||||
.sorted(Comparator.comparing(ApprovalTask::getStepNo))
|
||||
@@ -171,7 +172,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
||||
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
||||
taskVo.setActedBy(item.getActedBy());
|
||||
taskVo.setActedByName(accountNameMap.get(item.getActedBy()));
|
||||
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
|
||||
taskVo.setActedAt(item.getActedAt());
|
||||
taskVo.setComment(item.getComment());
|
||||
return taskVo;
|
||||
@@ -185,7 +186,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
logVo.setId(item.getId());
|
||||
logVo.setEventType(item.getEventType());
|
||||
logVo.setOperatorId(item.getOperatorId());
|
||||
logVo.setOperatorName(accountNameMap.get(item.getOperatorId()));
|
||||
logVo.setOperatorAccount(resolveAccountLoginName(accountMap.get(item.getOperatorId())));
|
||||
logVo.setOperatorName(resolveAccountName(accountMap.get(item.getOperatorId())));
|
||||
logVo.setCreated(item.getCreated());
|
||||
logVo.setPayloadJson(item.getPayloadJson());
|
||||
return logVo;
|
||||
@@ -205,14 +207,14 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载审批详情里涉及到的账号显示名称。
|
||||
* 批量加载审批详情里涉及到的账号信息。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param tasks 审批任务列表
|
||||
* @param logs 审批日志列表
|
||||
* @return 账号 ID 到展示名称的映射
|
||||
* @return 账号 ID 到账号实体的映射
|
||||
*/
|
||||
private Map<BigInteger, String> loadAccountNameMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
||||
private Map<BigInteger, SysAccount> loadAccountMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
||||
List<ApprovalLog> logs) {
|
||||
Set<BigInteger> accountIds = new HashSet<>();
|
||||
if (instance.getApplicantId() != null) {
|
||||
@@ -232,7 +234,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
return sysAccountService.listByIds(accountIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
SysAccount::getId,
|
||||
this::resolveAccountName,
|
||||
account -> account,
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
@@ -256,6 +258,19 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析账号登录名。
|
||||
*
|
||||
* @param account 账号实体
|
||||
* @return 登录账号
|
||||
*/
|
||||
private String resolveAccountLoginName(SysAccount account) {
|
||||
if (account == null || !StringUtils.hasText(account.getLoginName())) {
|
||||
return null;
|
||||
}
|
||||
return account.getLoginName().trim();
|
||||
}
|
||||
|
||||
private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
if (StringUtils.hasText(resourceType)) {
|
||||
|
||||
@@ -15,7 +15,11 @@ import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -32,6 +36,16 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
||||
@Resource
|
||||
private SysAccountRoleService sysAccountRoleService;
|
||||
|
||||
/**
|
||||
* 根据账号查询菜单,并自动补齐已授权节点的父级菜单链。
|
||||
* <p>
|
||||
* 这样当角色只勾选了某个页面下的按钮权限或子能力时,
|
||||
* 其所属的页面菜单仍能正常出现在侧边栏中,避免出现“有子权限但无入口”的问题。
|
||||
*
|
||||
* @param entity 菜单过滤条件
|
||||
* @param accountId 账号 ID
|
||||
* @return 当前账号可访问的菜单集合
|
||||
*/
|
||||
@Override
|
||||
public List<SysMenu> getMenusByAccountId(SysMenu entity, BigInteger accountId) {
|
||||
// 查询用户对应角色id集合
|
||||
@@ -48,11 +62,53 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
||||
if (CollectionUtil.isEmpty(menuIds)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<BigInteger> fullMenuIds = collectMenuIdsWithParents(menuIds);
|
||||
// 查询当前用户拥有的菜单
|
||||
SqlOperators ops = SqlOperatorsUtil.build(SysMenu.class);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, ops);
|
||||
queryWrapper.in(SysMenu::getId, menuIds);
|
||||
queryWrapper.in(SysMenu::getId, fullMenuIds);
|
||||
queryWrapper.orderBy("sort_no asc");
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集菜单自身及其所有父级菜单 ID。
|
||||
*
|
||||
* @param menuIds 已授权的菜单 ID 列表
|
||||
* @return 包含父级链路的完整菜单 ID 列表
|
||||
*/
|
||||
private List<BigInteger> collectMenuIdsWithParents(List<BigInteger> menuIds) {
|
||||
List<SysMenu> allMenus = list();
|
||||
if (CollectionUtil.isEmpty(allMenus)) {
|
||||
return menuIds;
|
||||
}
|
||||
Map<BigInteger, SysMenu> menuMap = new HashMap<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
menuMap.put(menu.getId(), menu);
|
||||
}
|
||||
Set<BigInteger> result = new HashSet<>(menuIds);
|
||||
for (BigInteger menuId : menuIds) {
|
||||
appendParentMenuIds(menuId, menuMap, result);
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归追加父级菜单 ID,直到根节点或无父节点为止。
|
||||
*
|
||||
* @param menuId 当前菜单 ID
|
||||
* @param menuMap 全量菜单映射
|
||||
* @param result 结果集合
|
||||
*/
|
||||
private void appendParentMenuIds(BigInteger menuId, Map<BigInteger, SysMenu> menuMap, Set<BigInteger> result) {
|
||||
SysMenu current = menuMap.get(menuId);
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
BigInteger parentId = current.getParentId();
|
||||
if (parentId == null || BigInteger.ZERO.equals(parentId) || !result.add(parentId)) {
|
||||
return;
|
||||
}
|
||||
appendParentMenuIds(parentId, menuMap, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE `tb_sys_menu`
|
||||
SET
|
||||
`menu_url` = '/sys/approval',
|
||||
`component` = '/system/approval/ApprovalManage',
|
||||
`menu_icon` = 'svg:approval',
|
||||
`is_show` = 1,
|
||||
`modified` = NOW(),
|
||||
`modified_by` = 1,
|
||||
`remark` = '审批管理菜单'
|
||||
WHERE `id` = 367100000000000001;
|
||||
|
||||
UPDATE `tb_sys_menu`
|
||||
SET
|
||||
`menu_type` = 1,
|
||||
`menu_url` = '',
|
||||
`component` = '',
|
||||
`menu_icon` = '',
|
||||
`is_show` = 0,
|
||||
`permission_tag` = '/page/approval/flow',
|
||||
`modified` = NOW(),
|
||||
`modified_by` = 1,
|
||||
`remark` = '审批管理-流程配置页签权限'
|
||||
WHERE `id` = 367100000000000002;
|
||||
|
||||
UPDATE `tb_sys_menu`
|
||||
SET
|
||||
`menu_type` = 1,
|
||||
`menu_url` = '',
|
||||
`component` = '',
|
||||
`menu_icon` = '',
|
||||
`is_show` = 0,
|
||||
`permission_tag` = '/page/approval/pending',
|
||||
`modified` = NOW(),
|
||||
`modified_by` = 1,
|
||||
`remark` = '审批管理-待审批页签权限'
|
||||
WHERE `id` = 367100000000000003;
|
||||
|
||||
UPDATE `tb_sys_menu`
|
||||
SET
|
||||
`menu_type` = 1,
|
||||
`menu_url` = '',
|
||||
`component` = '',
|
||||
`menu_icon` = '',
|
||||
`is_show` = 0,
|
||||
`permission_tag` = '/page/approval/processed',
|
||||
`modified` = NOW(),
|
||||
`modified_by` = 1,
|
||||
`remark` = '审批管理-已审批页签权限'
|
||||
WHERE `id` = 367100000000000004;
|
||||
|
||||
UPDATE `tb_sys_menu`
|
||||
SET
|
||||
`menu_type` = 1,
|
||||
`menu_url` = '',
|
||||
`component` = '',
|
||||
`menu_icon` = '',
|
||||
`is_show` = 0,
|
||||
`permission_tag` = '/page/approval/initiated',
|
||||
`modified` = NOW(),
|
||||
`modified_by` = 1,
|
||||
`remark` = '审批管理-我发起页签权限'
|
||||
WHERE `id` = 367100000000000005;
|
||||
@@ -61,6 +61,14 @@ export const submitBotPublishApproval = (id: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
/** 提交 Bot 下线审批 */
|
||||
export const submitBotOfflineApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/bot/submitOfflineApproval',
|
||||
{ id },
|
||||
);
|
||||
};
|
||||
|
||||
/** 提交 Bot 删除审批 */
|
||||
export const submitBotDeleteApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ActionButton {
|
||||
permission?: string;
|
||||
placement?: ActionPlacement;
|
||||
tone?: ActionTone;
|
||||
visible?: ((row: any) => boolean) | boolean;
|
||||
onClick: (row: any) => void;
|
||||
}
|
||||
|
||||
@@ -77,6 +78,13 @@ function hasPermission(permission?: string) {
|
||||
return !permission || hasAccessByCodes([permission]);
|
||||
}
|
||||
|
||||
function isActionVisible(action: ActionButton, row: any) {
|
||||
if (typeof action.visible === 'function') {
|
||||
return action.visible(row);
|
||||
}
|
||||
return action.visible !== false;
|
||||
}
|
||||
|
||||
const resolvedPrimaryAction = computed(() => {
|
||||
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
|
||||
return undefined;
|
||||
@@ -109,24 +117,6 @@ const resolvedActions = computed<ResolvedActionButton[]>(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const inlineActions = computed(() => {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'inline',
|
||||
);
|
||||
});
|
||||
|
||||
const menuActions = computed(() => {
|
||||
return resolvedActions.value.filter((action) => action.placement === 'menu');
|
||||
});
|
||||
|
||||
const showFooter = computed(() => {
|
||||
return Boolean(
|
||||
resolvedPrimaryAction.value ||
|
||||
inlineActions.value.length > 0 ||
|
||||
menuActions.value.length > 0,
|
||||
);
|
||||
});
|
||||
|
||||
function handlePrimaryAction(item: any) {
|
||||
resolvedPrimaryAction.value?.onClick(item);
|
||||
}
|
||||
@@ -139,6 +129,24 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
function resolveActionText(action: ActionButton, item: any) {
|
||||
return typeof action.text === 'function' ? action.text(item) : action.text;
|
||||
}
|
||||
|
||||
function resolveInlineActions(item: any) {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'inline' && isActionVisible(action, item),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMenuActions(item: any) {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'menu' && isActionVisible(action, item),
|
||||
);
|
||||
}
|
||||
|
||||
function hasVisibleActions(item: any) {
|
||||
return (
|
||||
resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -198,7 +206,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="showFooter" #footer>
|
||||
<template v-if="resolvedPrimaryAction || hasVisibleActions(item)" #footer>
|
||||
<div class="card-footer">
|
||||
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
|
||||
<div class="primary-label">
|
||||
@@ -214,12 +222,12 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="inlineActions.length > 0 || menuActions.length > 0"
|
||||
v-if="hasVisibleActions(item)"
|
||||
class="card-actions"
|
||||
@click.stop
|
||||
>
|
||||
<ElButton
|
||||
v-for="(action, actionIndex) in inlineActions"
|
||||
v-for="(action, actionIndex) in resolveInlineActions(item)"
|
||||
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
||||
:icon="typeof action.icon === 'string' ? undefined : action.icon"
|
||||
size="small"
|
||||
@@ -235,7 +243,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown
|
||||
v-if="menuActions.length > 0"
|
||||
v-if="resolveMenuActions(item).length > 0"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
>
|
||||
@@ -248,7 +256,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="(action, actionIndex) in menuActions"
|
||||
v-for="(action, actionIndex) in resolveMenuActions(item)"
|
||||
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
||||
:class="{
|
||||
'card-menu-item--danger': action.tone === 'danger',
|
||||
|
||||
@@ -65,10 +65,16 @@
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOffline": "Offline",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusLabel": "Release",
|
||||
"submitPublishApprovalConfirm": "The current draft will enter the publish approval flow. It becomes externally available only after approval.",
|
||||
"submitDeleteApprovalConfirm": "The workflow will enter the delete approval flow. It will be physically deleted only after approval.",
|
||||
"submitPublishApprovalConfirm": "Publish the current workflow now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
||||
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
||||
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||
"check": "Check",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"action": {
|
||||
"publish": "Publish",
|
||||
"offline": "Offline",
|
||||
"delete": "Delete",
|
||||
"addFlow": "New Flow",
|
||||
"editFlow": "Edit Flow",
|
||||
@@ -79,7 +80,7 @@
|
||||
"actedAt": "Acted At",
|
||||
"comment": "Comment",
|
||||
"eventType": "Event Type",
|
||||
"operatorId": "Operator ID",
|
||||
"operatorId": "Operator Account",
|
||||
"operatorName": "Operator Name",
|
||||
"createdAt": "Created At",
|
||||
"eventInfo": "Event Info",
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
"publishStatusPublishPendingDesc": "The assistant will switch to the new release after approval.",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusPublishedDesc": "The current release is externally available. Ongoing edits still stay in draft.",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOfflinePendingDesc": "The current release stays available until approval completes, but it is hidden from new binding candidates.",
|
||||
"publishStatusOffline": "Offline",
|
||||
"publishStatusOfflineDesc": "The current release is offline. External chat, Public API, and new bindings are unavailable.",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusDeletePendingDesc": "The current release remains available, but it is no longer offered as a new binding candidate.",
|
||||
"submitPublishApprovalConfirm": "Submit the current draft to publish approval. It becomes externally available only after approval.",
|
||||
"submitDeleteApprovalConfirm": "Submit the bot to delete approval. It will be physically deleted only after approval.",
|
||||
"submitPublishApprovalConfirm": "Publish the current assistant now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current assistant now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current assistant offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current assistant?",
|
||||
"publishPendingHint": "There is already an approval in progress for this bot.",
|
||||
"deletePendingHint": "There is already an approval in progress for this bot.",
|
||||
"publishRequiredHint": "There is no released version yet. Submit publish approval first.",
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
"markAsResolved": "MarkAsResolved",
|
||||
"optimizing": "Optimizing",
|
||||
"regenerate": "Regenerate",
|
||||
"publish": "Publish",
|
||||
"republish": "Republish",
|
||||
"offline": "Offline",
|
||||
"hide": "Hide",
|
||||
"more": "Mode",
|
||||
"submitDeleteApproval": "Submit Delete Approval",
|
||||
|
||||
@@ -38,10 +38,18 @@
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOffline": "Offline",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusLabel": "Release",
|
||||
"submitPublishApprovalConfirm": "The knowledge base will enter the publish approval flow. It can be referenced by bots only after approval.",
|
||||
"submitDeleteApprovalConfirm": "The knowledge base will enter the delete approval flow. It will be physically deleted only after approval.",
|
||||
"submitPublishApprovalConfirm": "Publish the current knowledge base now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current knowledge base now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current knowledge base offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current knowledge base?",
|
||||
"offlineImpactBoundBotsIntro": "This knowledge base is currently bound to the following bots:",
|
||||
"offlineImpactBoundBotsFooter": "After the knowledge base goes offline, the system will automatically remove it from these bots.",
|
||||
"offlineImpactWorkflowBlockedIntro": "This knowledge base is still used by the following workflows:",
|
||||
"offlineImpactWorkflowBlockedFooter": "Please update those workflow nodes before taking the knowledge base offline.",
|
||||
"publishPendingHint": "There is already an approval in progress for this knowledge base.",
|
||||
"deletePendingHint": "There is already an approval in progress for this knowledge base.",
|
||||
"createdModifyTime": "Creation/update time",
|
||||
|
||||
@@ -65,10 +65,16 @@
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后新版本才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
||||
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"check": "检查",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"action": {
|
||||
"publish": "发布",
|
||||
"offline": "下线",
|
||||
"delete": "删除",
|
||||
"addFlow": "新建流程",
|
||||
"editFlow": "编辑流程",
|
||||
@@ -79,7 +80,7 @@
|
||||
"actedAt": "处理时间",
|
||||
"comment": "处理意见",
|
||||
"eventType": "事件类型",
|
||||
"operatorId": "操作人ID",
|
||||
"operatorId": "操作人账号",
|
||||
"operatorName": "操作人名称",
|
||||
"createdAt": "创建时间",
|
||||
"eventInfo": "事件信息",
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
"publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOfflinePendingDesc": "审批完成前当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusOfflineDesc": "当前正式版本已下线,外链聊天、Public API 和新的资源绑定都不可用。",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后聊天助手才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前聊天助手吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前聊天助手吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前聊天助手吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前聊天助手吗?",
|
||||
"publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。",
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
"markAsResolved": "标记已处理",
|
||||
"optimizing": "正在优化中...",
|
||||
"regenerate": "重新生成",
|
||||
"publish": "发布",
|
||||
"republish": "重新发布",
|
||||
"offline": "下线",
|
||||
"hide": "隐藏",
|
||||
"more": "更多",
|
||||
"submitDeleteApproval": "提交删除审批",
|
||||
|
||||
@@ -38,10 +38,18 @@
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后该知识库才可作为正式版本被聊天助手引用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前知识库吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前知识库吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前知识库吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前知识库吗?",
|
||||
"offlineImpactBoundBotsIntro": "当前知识库被以下聊天助手绑定:",
|
||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该知识库。",
|
||||
"offlineImpactWorkflowBlockedIntro": "当前知识库仍被以下工作流使用:",
|
||||
"offlineImpactWorkflowBlockedFooter": "请先在工作流中调整相关知识库节点后再下线。",
|
||||
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"createdModifyTime": "创建/更新时间",
|
||||
|
||||
@@ -243,6 +243,10 @@ function setupAccessGuard(router: Router) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 页面菜单与按钮权限码是两套数据源。每次重新构建动态菜单时,
|
||||
// 同步刷新一次 accessCodes,避免后端权限模型调整后页面仍持有旧按钮权限。
|
||||
await authStore.fetchAccessCodes();
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
|
||||
@@ -3,6 +3,74 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'ApprovalFlowPage',
|
||||
path: '/sys/approval/flow',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'flow',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.flow'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalPendingPage',
|
||||
path: '/sys/approval/pending',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'pending',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.pending'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalProcessedPage',
|
||||
path: '/sys/approval/processed',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'processed',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.processed'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalInitiatedPage',
|
||||
path: '/sys/approval/initiated',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'initiated',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.initiated'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalDetail',
|
||||
path: '/sys/approval/detail/:id',
|
||||
|
||||
@@ -154,6 +154,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
async function fetchAccessCodes() {
|
||||
const accessCodes = await getAccessCodesApi();
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
return accessCodes;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
@@ -162,6 +168,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
$reset,
|
||||
authDevLogin,
|
||||
authLogin,
|
||||
fetchAccessCodes,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api';
|
||||
import {
|
||||
submitBotDeleteApproval,
|
||||
submitBotOfflineApproval,
|
||||
submitBotPublishApproval,
|
||||
} from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
@@ -34,9 +38,12 @@ import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
@@ -105,35 +112,57 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row: BotInfo) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/bot/save',
|
||||
placement: 'inline',
|
||||
visible: (row: BotInfo) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleSubmitPublishApproval(row);
|
||||
handlePublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/bot/save',
|
||||
placement: 'menu',
|
||||
visible: (row: BotInfo) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/bot/remove',
|
||||
placement: 'menu',
|
||||
visible: (row: BotInfo) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleSubmitDeleteApproval(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
||||
function isRepublishAction(bot: BotInfo) {
|
||||
return canAiResourceRepublish(bot.displayPublishStatus, bot.publishStatus);
|
||||
}
|
||||
|
||||
const handlePublishAction = async (bot: BotInfo) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(bot)
|
||||
? $t('bot.submitRepublishApprovalConfirm')
|
||||
: $t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -150,8 +179,36 @@ const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
const handleOfflineAction = async (bot: BotInfo) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotOfflineApproval(String(bot.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -174,8 +231,11 @@ const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMetaByInstance(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
@@ -186,6 +246,16 @@ function resolvePublishStatusMeta(status?: string) {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
type: 'warning' as const,
|
||||
};
|
||||
case 'OFFLINE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusOfflinePending'),
|
||||
type: 'warning' as const,
|
||||
};
|
||||
case 'OFFLINE':
|
||||
return {
|
||||
label: $t('bot.publishStatusOffline'),
|
||||
type: 'info' as const,
|
||||
};
|
||||
case 'DELETE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusDeletePending'),
|
||||
@@ -378,9 +448,9 @@ const getSideList = async () => {
|
||||
size="small"
|
||||
effect="plain"
|
||||
round
|
||||
:type="resolvePublishStatusMeta(item.publishStatus).type"
|
||||
:type="resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).type"
|
||||
>
|
||||
{{ resolvePublishStatusMeta(item.publishStatus).label }}
|
||||
{{ resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</CardList>
|
||||
|
||||
@@ -42,6 +42,7 @@ import { tryit } from 'radash';
|
||||
import {
|
||||
getPerQuestions,
|
||||
submitBotDeleteApproval,
|
||||
submitBotOfflineApproval,
|
||||
submitBotPublishApproval,
|
||||
updateBotApi,
|
||||
updateBotOptions,
|
||||
@@ -56,10 +57,13 @@ import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDa
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourceExternallyVisible,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
interface SelectedMcpTool {
|
||||
@@ -163,18 +167,42 @@ const publicChatUrl = computed(() => {
|
||||
const publicChatEmbedUrl = computed(() => {
|
||||
return buildPublicChatUrl(true);
|
||||
});
|
||||
const botDisplayPublishStatus = computed(() =>
|
||||
resolveAiResourceDisplayStatus(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const botApprovalPending = computed(() =>
|
||||
isAiResourceApprovalPending(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const publishStatusMeta = computed<{
|
||||
description: string;
|
||||
label: string;
|
||||
type: 'danger' | 'info' | 'success' | 'warning';
|
||||
}>(() => {
|
||||
switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) {
|
||||
switch (botDisplayPublishStatus.value) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
type: 'success',
|
||||
description: $t('bot.publishStatusPublishedDesc'),
|
||||
};
|
||||
case 'OFFLINE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusOfflinePending'),
|
||||
type: 'warning',
|
||||
description: $t('bot.publishStatusOfflinePendingDesc'),
|
||||
};
|
||||
case 'OFFLINE':
|
||||
return {
|
||||
label: $t('bot.publishStatusOffline'),
|
||||
type: 'info',
|
||||
description: $t('bot.publishStatusOfflineDesc'),
|
||||
};
|
||||
case 'PUBLISH_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
@@ -199,9 +227,44 @@ const canUsePublicAccess = computed(() =>
|
||||
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
|
||||
);
|
||||
const publishPrimaryActionLabel = computed(() =>
|
||||
isAiResourcePublished(botInfo.value?.publishStatus)
|
||||
canAiResourceRepublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
);
|
||||
const canShowPublishPrimaryAction = computed(() =>
|
||||
canAiResourcePublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
) ||
|
||||
canAiResourceRepublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const secondaryActionLabel = computed(() => {
|
||||
if (
|
||||
canAiResourceOffline(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
return $t('button.offline');
|
||||
}
|
||||
if (
|
||||
canAiResourceDelete(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
return $t('button.delete');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const canShowSecondaryAction = computed(() =>
|
||||
Boolean(secondaryActionLabel.value),
|
||||
);
|
||||
const iframeCode = computed(() => {
|
||||
if (!publicChatEmbedUrl.value) {
|
||||
@@ -820,17 +883,24 @@ const handleDeletePresetQuestion = (item: any) => {
|
||||
const handlePublishWx = () => {
|
||||
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
|
||||
};
|
||||
const handleSubmitPublishApproval = async () => {
|
||||
const handleLifecycleAction = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
if (
|
||||
botApprovalPending.value
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
canAiResourceRepublish(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
)
|
||||
? $t('bot.submitRepublishApprovalConfirm')
|
||||
: $t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -849,17 +919,32 @@ const handleSubmitPublishApproval = async () => {
|
||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||
}
|
||||
};
|
||||
const handleSubmitDeleteApproval = async () => {
|
||||
const handleSecondaryAction = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
if (
|
||||
botApprovalPending.value
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const canOffline = canAiResourceOffline(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
);
|
||||
const canDelete = canAiResourceDelete(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
);
|
||||
if (!canOffline && !canDelete) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitDeleteApprovalConfirm'),
|
||||
canOffline
|
||||
? $t('bot.submitOfflineApprovalConfirm')
|
||||
: $t('bot.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -870,7 +955,9 @@ const handleSubmitDeleteApproval = async () => {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotDeleteApproval(String(botInfo.value.id));
|
||||
const res = canOffline
|
||||
? await submitBotOfflineApproval(String(botInfo.value.id))
|
||||
: await submitBotDeleteApproval(String(botInfo.value.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
getBotDetail();
|
||||
@@ -1485,19 +1572,21 @@ const handleBasicInfoChange = async (
|
||||
</div>
|
||||
<div class="publish-summary-actions">
|
||||
<ElButton
|
||||
v-if="canShowPublishPrimaryAction"
|
||||
type="primary"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitPublishApproval"
|
||||
@click="handleLifecycleAction"
|
||||
>
|
||||
{{ publishPrimaryActionLabel }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="canShowSecondaryAction"
|
||||
plain
|
||||
type="danger"
|
||||
:type="secondaryActionLabel === $t('button.delete') ? 'danger' : 'default'"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitDeleteApproval"
|
||||
@click="handleSecondaryAction"
|
||||
>
|
||||
{{ $t('button.submitDeleteApproval') }}
|
||||
{{ secondaryActionLabel }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,16 @@ import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
buildOfflineImpactMessage,
|
||||
type OfflineImpactCheck,
|
||||
} from '#/views/ai/shared/offline-impact';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -186,24 +193,41 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'inline',
|
||||
visible: (row) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
submitPublishApproval(row);
|
||||
submitPublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'menu',
|
||||
visible: (row) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
submitOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.delete'),
|
||||
icon: Delete,
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
placement: 'menu',
|
||||
visible: (row) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
@@ -216,14 +240,22 @@ const actions: ActionButton[] = [
|
||||
onMounted(() => {
|
||||
getCategoryList();
|
||||
});
|
||||
const submitPublishApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
function isRepublishAction(item: any) {
|
||||
return canAiResourceRepublish(item.displayPublishStatus, item.publishStatus);
|
||||
}
|
||||
|
||||
const submitPublishAction = async (item: any) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('documentCollection.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(item)
|
||||
? $t('documentCollection.submitRepublishApprovalConfirm')
|
||||
: $t('documentCollection.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -245,8 +277,71 @@ const submitPublishApproval = async (item: any) => {
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
const submitOfflineAction = async (item: any) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const impactRes = await api.get<{
|
||||
data: OfflineImpactCheck;
|
||||
errorCode: number;
|
||||
}>(
|
||||
'/api/v1/documentCollection/offlineImpactCheck',
|
||||
{
|
||||
params: { id: item.id },
|
||||
},
|
||||
);
|
||||
if (impactRes.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
if (impactRes.data?.hasWorkflowUsages) {
|
||||
await ElMessageBox.alert(
|
||||
buildOfflineImpactMessage(
|
||||
$t('documentCollection.offlineImpactWorkflowBlockedIntro'),
|
||||
impactRes.data.workflowUsages,
|
||||
$t('documentCollection.offlineImpactWorkflowBlockedFooter'),
|
||||
),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
impactRes.data?.hasBotBindings
|
||||
? buildOfflineImpactMessage(
|
||||
$t('documentCollection.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
$t('documentCollection.offlineImpactBoundBotsFooter'),
|
||||
)
|
||||
: $t('documentCollection.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/documentCollection/submitOfflineApproval', {
|
||||
id: item.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
const submitDeleteApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -274,14 +369,29 @@ const submitDeleteApproval = async (item: any) => {
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMeta(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusOfflinePending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusOffline'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusPublishPending'),
|
||||
@@ -553,11 +663,11 @@ function changeCategory(category: any) {
|
||||
<template #publish>
|
||||
<div
|
||||
class="knowledge-publish-chip"
|
||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="knowledge-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,22 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
import { ElButton, ElEmpty, ElImage, ElScrollbar } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const data = ref<any>();
|
||||
function openDialog(row: any) {
|
||||
const docPreviewLoading = ref(false);
|
||||
const docPreviewContent = ref('');
|
||||
const docPreviewTruncated = ref(false);
|
||||
const docPreviewError = ref('');
|
||||
let previewRequestId = 0;
|
||||
|
||||
const isDocument = computed(() => data.value?.resourceType === 3);
|
||||
const fileName = computed(() => {
|
||||
const resourceName = data.value?.resourceName || '';
|
||||
const suffix = data.value?.suffix || '';
|
||||
return suffix ? `${resourceName}.${suffix}` : resourceName;
|
||||
});
|
||||
const previewWidth = computed(() => (isDocument.value ? 'xl' : 'md'));
|
||||
|
||||
async function openDialog(row: any) {
|
||||
data.value = row;
|
||||
dialogVisible.value = true;
|
||||
resetDocumentPreview();
|
||||
if (row?.resourceType === 3) {
|
||||
await loadDocumentPreview(row);
|
||||
}
|
||||
}
|
||||
function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function resetDocumentPreview() {
|
||||
docPreviewLoading.value = false;
|
||||
docPreviewContent.value = '';
|
||||
docPreviewTruncated.value = false;
|
||||
docPreviewError.value = '';
|
||||
}
|
||||
|
||||
async function loadDocumentPreview(row: any) {
|
||||
if (!row?.id) {
|
||||
docPreviewError.value = '当前素材缺少预览标识,请下载后查看';
|
||||
return;
|
||||
}
|
||||
const currentRequestId = ++previewRequestId;
|
||||
docPreviewLoading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/resource/previewContent', {
|
||||
params: { id: row.id },
|
||||
});
|
||||
if (currentRequestId !== previewRequestId) {
|
||||
return;
|
||||
}
|
||||
docPreviewContent.value = res.data?.content || '';
|
||||
docPreviewTruncated.value = !!res.data?.truncated;
|
||||
if (!docPreviewContent.value) {
|
||||
docPreviewError.value = '暂未提取到可预览内容,请下载后查看';
|
||||
}
|
||||
} catch {
|
||||
if (currentRequestId !== previewRequestId) {
|
||||
return;
|
||||
}
|
||||
docPreviewError.value = '文档预览加载失败,请下载后查看';
|
||||
} finally {
|
||||
if (currentRequestId === previewRequestId) {
|
||||
docPreviewLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openSourceFile() {
|
||||
if (data.value?.resourceUrl) {
|
||||
window.open(data.value.resourceUrl, '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,25 +89,119 @@ function closeDialog() {
|
||||
v-model:open="dialogVisible"
|
||||
:title="$t('message.preview')"
|
||||
:before-close="closeDialog"
|
||||
width="md"
|
||||
:width="previewWidth"
|
||||
:show-footer="false"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<div class="resource-preview flex justify-center">
|
||||
<ElImage
|
||||
v-if="data.resourceType === 0"
|
||||
style="width: 200px"
|
||||
:preview-src-list="[data.resourceUrl]"
|
||||
:src="data.resourceUrl"
|
||||
/>
|
||||
<video v-if="data.resourceType === 1" controls width="640" height="360">
|
||||
<video
|
||||
v-else-if="data.resourceType === 1"
|
||||
controls
|
||||
width="640"
|
||||
height="360"
|
||||
>
|
||||
<source :src="data.resourceUrl" type="video/mp4" />
|
||||
{{ $t('message.notVideo') }}
|
||||
</video>
|
||||
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
|
||||
<audio
|
||||
v-else-if="data.resourceType === 2"
|
||||
controls
|
||||
class="mt-8 w-full max-w-[640px]"
|
||||
:src="data.resourceUrl"
|
||||
>
|
||||
{{ $t('message.notAudio') }}
|
||||
</audio>
|
||||
<div
|
||||
v-else-if="isDocument"
|
||||
v-loading="docPreviewLoading"
|
||||
:element-loading-text="$t('message.loading')"
|
||||
class="resource-preview__document bg-background border-border w-full rounded-xl border"
|
||||
>
|
||||
<div
|
||||
class="resource-preview__toolbar border-border flex items-center justify-between gap-3 border-b px-5 py-4"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-medium">{{ fileName }}</div>
|
||||
<div
|
||||
v-if="docPreviewTruncated"
|
||||
class="text-muted-foreground mt-1 text-xs"
|
||||
>
|
||||
内容较长,当前仅展示前 20000 个字符
|
||||
</div>
|
||||
</div>
|
||||
<ElButton link type="primary" @click="openSourceFile">
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="resource-preview__body">
|
||||
<ElEmpty
|
||||
v-if="docPreviewError"
|
||||
:description="docPreviewError"
|
||||
class="resource-preview__empty"
|
||||
>
|
||||
<ElButton link type="primary" @click="openSourceFile">
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElEmpty>
|
||||
<ElScrollbar
|
||||
v-else
|
||||
class="resource-preview__scrollbar"
|
||||
wrap-class="resource-preview__scrollbar-wrap"
|
||||
>
|
||||
<pre class="resource-preview__content">{{
|
||||
docPreviewContent
|
||||
}}</pre>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EasyFlowPanelModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.resource-preview {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.resource-preview__document {
|
||||
min-height: 540px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resource-preview__toolbar {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.resource-preview__body {
|
||||
height: 468px;
|
||||
}
|
||||
|
||||
.resource-preview__scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.resource-preview__scrollbar-wrap) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resource-preview__content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family:
|
||||
'SFMono-Regular', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono',
|
||||
monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.resource-preview__empty {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { h } from 'vue';
|
||||
|
||||
export interface OfflineImpactBinding {
|
||||
id?: number | string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface OfflineImpactCheck {
|
||||
canProceed: boolean;
|
||||
hasBotBindings: boolean;
|
||||
hasWorkflowUsages: boolean;
|
||||
botBindings: OfflineImpactBinding[];
|
||||
workflowUsages: OfflineImpactBinding[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function resolveTitle(item: OfflineImpactBinding) {
|
||||
return item.title || String(item.id || '');
|
||||
}
|
||||
|
||||
export function joinOfflineImpactTitles(items: OfflineImpactBinding[] = []) {
|
||||
return items.map(resolveTitle).filter(Boolean).join('、');
|
||||
}
|
||||
|
||||
export function buildOfflineImpactMessage(
|
||||
intro: string,
|
||||
items: OfflineImpactBinding[] = [],
|
||||
footer?: string,
|
||||
) {
|
||||
return h('div', [
|
||||
h('p', intro),
|
||||
h(
|
||||
'ul',
|
||||
items.map((item) => h('li', { key: String(item.id || item.title || '') }, resolveTitle(item))),
|
||||
),
|
||||
footer ? h('p', footer) : null,
|
||||
]);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export type AiPublishStatus =
|
||||
| 'DELETE_PENDING'
|
||||
| 'DRAFT'
|
||||
| 'OFFLINE'
|
||||
| 'OFFLINE_PENDING'
|
||||
| 'PUBLISHED'
|
||||
| 'PUBLISH_PENDING';
|
||||
|
||||
@@ -13,6 +15,8 @@ export function normalizeAiPublishStatus(
|
||||
switch (value) {
|
||||
case 'PUBLISHED':
|
||||
case 'PUBLISH_PENDING':
|
||||
case 'OFFLINE':
|
||||
case 'OFFLINE_PENDING':
|
||||
case 'DELETE_PENDING':
|
||||
return value;
|
||||
default:
|
||||
@@ -20,6 +24,17 @@ export function normalizeAiPublishStatus(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用于页面展示的发布状态。
|
||||
* 已发布资源若存在当前审批实例,则视为“发布审批中”,用于统一状态文案与动作禁用。
|
||||
*/
|
||||
export function resolveAiResourceDisplayStatus(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
): AiPublishStatus {
|
||||
return normalizeAiPublishStatus(displayValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否已有正式线上版本。
|
||||
*/
|
||||
@@ -27,12 +42,23 @@ export function isAiResourcePublished(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否处于已下线状态。
|
||||
*/
|
||||
export function isAiResourceOffline(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'OFFLINE';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许对外可见。
|
||||
*/
|
||||
export function isAiResourceExternallyVisible(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISHED' || normalized === 'DELETE_PENDING';
|
||||
return (
|
||||
normalized === 'PUBLISHED' ||
|
||||
normalized === 'DELETE_PENDING' ||
|
||||
normalized === 'OFFLINE_PENDING'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +71,68 @@ export function isAiResourceSelectableForBot(value?: null | string) {
|
||||
/**
|
||||
* 当前资源是否处于审批处理中。
|
||||
*/
|
||||
export function isAiResourceApprovalPending(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISH_PENDING' || normalized === 'DELETE_PENDING';
|
||||
export function isAiResourceApprovalPending(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return (
|
||||
normalized === 'PUBLISH_PENDING' ||
|
||||
normalized === 'OFFLINE_PENDING' ||
|
||||
normalized === 'DELETE_PENDING'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起发布。
|
||||
*/
|
||||
export function canAiResourcePublish(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许重新发布。
|
||||
*/
|
||||
export function canAiResourceRepublish(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起下线。
|
||||
*/
|
||||
export function canAiResourceOffline(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起删除。
|
||||
*/
|
||||
export function canAiResourceDelete(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import {
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||
@@ -251,18 +252,26 @@ const updatePluginNode = ref<any>(null);
|
||||
const pageLoading = ref(false);
|
||||
const chainInfo = ref<any>(null);
|
||||
const publishActionText = computed(() => {
|
||||
switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) {
|
||||
switch (
|
||||
resolveAiResourceDisplayStatus(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
case 'DELETE_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusDeletePending');
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusOfflinePending');
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusPublishPending');
|
||||
}
|
||||
case 'PUBLISHED': {
|
||||
return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`;
|
||||
return $t('button.republish');
|
||||
}
|
||||
default: {
|
||||
return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`;
|
||||
return $t('button.publish');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -272,7 +281,10 @@ const publishActionDisabled = computed(
|
||||
saveLoading.value ||
|
||||
checkLoading.value ||
|
||||
publishLoading.value ||
|
||||
isAiResourceApprovalPending(workflowInfo.value?.publishStatus),
|
||||
isAiResourceApprovalPending(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
|
||||
function syncNavTitle(title: string) {
|
||||
@@ -498,17 +510,27 @@ function closeCheckIssues() {
|
||||
async function handleCheck() {
|
||||
await runCheck('PRE_EXECUTE');
|
||||
}
|
||||
async function handlePublish() {
|
||||
async function handlePublishAction() {
|
||||
if (publishLoading.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
canAiResourceRepublish(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -680,8 +702,8 @@ function onAsyncExecute(info: any) {
|
||||
:loading="publishLoading"
|
||||
:disabled="publishActionDisabled"
|
||||
class="workflow-publish-button"
|
||||
:class="`workflow-publish-button--${normalizeAiPublishStatus(workflowInfo?.publishStatus)}`"
|
||||
@click="handlePublish"
|
||||
:class="`workflow-publish-button--${resolveAiResourceDisplayStatus(workflowInfo?.displayPublishStatus, workflowInfo?.publishStatus)}`"
|
||||
@click="handlePublishAction"
|
||||
>
|
||||
{{ publishActionText }}
|
||||
</ElButton>
|
||||
@@ -809,12 +831,24 @@ function onAsyncExecute(info: any) {
|
||||
border-color: hsl(var(--warning) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--OFFLINE_PENDING.el-button) {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 18%);
|
||||
border-color: hsl(var(--warning) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--PUBLISHED.el-button) {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 18%);
|
||||
border-color: hsl(var(--success) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--OFFLINE.el-button) {
|
||||
color: hsl(var(--foreground) / 78%);
|
||||
background: hsl(var(--muted) / 62%);
|
||||
border-color: hsl(var(--foreground) / 14%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 16%);
|
||||
|
||||
@@ -50,9 +50,16 @@ import { router } from '#/router';
|
||||
import { useDictStore } from '#/store';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
buildOfflineImpactMessage,
|
||||
type OfflineImpactCheck,
|
||||
} from '#/views/ai/shared/offline-impact';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import WorkflowModal from './WorkflowModal.vue';
|
||||
@@ -175,21 +182,35 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row: any) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
visible: (row: any) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitPublishApproval(row);
|
||||
submitPublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/workflow/remove',
|
||||
placement: 'menu',
|
||||
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitDeleteApproval(row);
|
||||
},
|
||||
@@ -282,14 +303,22 @@ function showDialog(row: any, importMode = false) {
|
||||
function resolveNavTitle(row: any) {
|
||||
return row?.title || row?.name || '';
|
||||
}
|
||||
async function submitPublishApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
function isRepublishAction(row: any) {
|
||||
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
|
||||
}
|
||||
|
||||
async function submitPublishAction(row: any) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(row)
|
||||
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -300,7 +329,56 @@ async function submitPublishApproval(row: any) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
||||
const res = await api.post(
|
||||
'/api/v1/workflow/submitPublishApproval',
|
||||
{
|
||||
id: row.id,
|
||||
},
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
async function submitOfflineAction(row: any) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const impactRes = await api.get<{
|
||||
data: OfflineImpactCheck;
|
||||
errorCode: number;
|
||||
}>(
|
||||
'/api/v1/workflow/offlineImpactCheck',
|
||||
{
|
||||
params: { id: row.id },
|
||||
},
|
||||
);
|
||||
if (impactRes.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
impactRes.data?.hasBotBindings
|
||||
? buildOfflineImpactMessage(
|
||||
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||
)
|
||||
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitOfflineApproval', {
|
||||
id: row.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
@@ -309,7 +387,9 @@ async function submitPublishApproval(row: any) {
|
||||
}
|
||||
}
|
||||
async function submitDeleteApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -334,14 +414,29 @@ async function submitDeleteApproval(row: any) {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMetaByInstance(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusOfflinePending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusOffline'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusPublishPending'),
|
||||
@@ -572,11 +667,11 @@ function handleHeaderButtonClick(data: any) {
|
||||
<template #publish>
|
||||
<div
|
||||
class="workflow-publish-chip"
|
||||
:class="`workflow-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
:class="`workflow-publish-chip--${resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="workflow-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +36,7 @@ const resourceLabelMap: Record<string, string> = {
|
||||
|
||||
const actionLabelMap: Record<string, string> = {
|
||||
DELETE: $t('approval.action.delete'),
|
||||
OFFLINE: $t('approval.action.offline'),
|
||||
PUBLISH: $t('approval.action.publish'),
|
||||
};
|
||||
|
||||
@@ -138,18 +139,28 @@ function formatPayload(payload: Record<string, any>) {
|
||||
return JSON.stringify(payload || {}, null, 2);
|
||||
}
|
||||
|
||||
function formatAccountDisplay(name?: string, id?: null | number | string) {
|
||||
if (name && id) {
|
||||
return `${name}(${id})`;
|
||||
function formatAccountDisplay(
|
||||
name?: string,
|
||||
account?: null | string,
|
||||
fallbackId?: null | number | string,
|
||||
) {
|
||||
if (name && account) {
|
||||
return `${name}(${account})`;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return id || '-';
|
||||
if (account) {
|
||||
return account;
|
||||
}
|
||||
return fallbackId || '-';
|
||||
}
|
||||
|
||||
function formatOperatorId(id?: null | number | string) {
|
||||
return id || '-';
|
||||
function formatOperatorId(
|
||||
account?: null | string,
|
||||
fallbackId?: null | number | string,
|
||||
) {
|
||||
return account || fallbackId || '-';
|
||||
}
|
||||
|
||||
function formatOperatorName(name?: null | string) {
|
||||
@@ -276,7 +287,13 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
{{ detail.id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
||||
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
|
||||
{{
|
||||
formatAccountDisplay(
|
||||
detail.applicantName,
|
||||
detail.applicantAccount,
|
||||
detail.applicantId,
|
||||
)
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
||||
{{ detail.submittedAt || '-' }}
|
||||
@@ -347,7 +364,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorId(row.operatorId) }}
|
||||
{{ formatOperatorId(row.operatorAccount, row.operatorId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
|
||||
@@ -31,7 +31,7 @@ defineExpose({
|
||||
});
|
||||
|
||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||
type ActionType = '' | 'DELETE' | 'PUBLISH';
|
||||
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||
type AssigneeType = 'ROLE' | 'USER';
|
||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||
@@ -77,6 +77,7 @@ const RESOURCE_OPTIONS = [
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||
];
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
@@ -40,6 +41,7 @@ const RESOURCE_OPTIONS = [
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||
];
|
||||
|
||||
@@ -66,21 +68,25 @@ const TAB_CONFIG = [
|
||||
label: $t('approval.tab.flow'),
|
||||
name: 'flow',
|
||||
path: '/sys/approval/flow',
|
||||
permission: '/page/approval/flow',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.pending'),
|
||||
name: 'pending',
|
||||
path: '/sys/approval/pending',
|
||||
permission: '/page/approval/pending',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.processed'),
|
||||
name: 'processed',
|
||||
path: '/sys/approval/processed',
|
||||
permission: '/page/approval/processed',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.initiated'),
|
||||
name: 'initiated',
|
||||
path: '/sys/approval/initiated',
|
||||
permission: '/page/approval/initiated',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -115,14 +121,23 @@ const pendingBadgeText = computed(() =>
|
||||
const hasApprovalRootMenu = computed(
|
||||
() => !!accessStore.getMenuByPath('/sys/approval'),
|
||||
);
|
||||
const hasLegacyTabMenus = computed(() =>
|
||||
TAB_CONFIG.some((item) => !!accessStore.getMenuByPath(item.path)),
|
||||
);
|
||||
const hasButtonTabPermissions = computed(() =>
|
||||
TAB_CONFIG.some((item) => hasPermission([item.permission])),
|
||||
);
|
||||
const visibleTabs = computed(() => {
|
||||
const matchedTabs = TAB_CONFIG.filter((item) =>
|
||||
accessStore.getMenuByPath(item.path),
|
||||
);
|
||||
if (matchedTabs.length > 0) {
|
||||
return matchedTabs;
|
||||
if (!hasApprovalRootMenu.value) {
|
||||
return [];
|
||||
}
|
||||
return hasApprovalRootMenu.value ? [...TAB_CONFIG] : [];
|
||||
if (hasButtonTabPermissions.value) {
|
||||
return TAB_CONFIG.filter((item) => hasPermission([item.permission]));
|
||||
}
|
||||
if (hasLegacyTabMenus.value) {
|
||||
return TAB_CONFIG.filter((item) => accessStore.getMenuByPath(item.path));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const visibleTabNames = computed(() =>
|
||||
visibleTabs.value.map((item) => item.name),
|
||||
@@ -139,7 +154,7 @@ const instanceStatusOptions = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void syncRouteTab(route.path);
|
||||
void syncRouteTab();
|
||||
});
|
||||
|
||||
watch(activeTab, () => {
|
||||
@@ -153,9 +168,9 @@ watch(activeTab, () => {
|
||||
}
|
||||
});
|
||||
watch(
|
||||
[() => route.path, visibleTabNames],
|
||||
async ([path]) => {
|
||||
await syncRouteTab(path);
|
||||
[() => route.fullPath, visibleTabNames],
|
||||
async () => {
|
||||
await syncRouteTab();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -177,19 +192,55 @@ function resolveTabNameByPath(path: string): ApprovalTabName {
|
||||
return 'flow';
|
||||
}
|
||||
|
||||
async function syncRouteTab(path: string) {
|
||||
function resolveTabNameByRoute() {
|
||||
const queryTab = String(route.query.tab || '');
|
||||
if (
|
||||
queryTab === 'flow' ||
|
||||
queryTab === 'pending' ||
|
||||
queryTab === 'processed' ||
|
||||
queryTab === 'initiated'
|
||||
) {
|
||||
return queryTab as ApprovalTabName;
|
||||
}
|
||||
return resolveTabNameByPath(route.path);
|
||||
}
|
||||
|
||||
async function syncRouteTab() {
|
||||
if (visibleTabs.value.length === 0) {
|
||||
activeTab.value = 'flow';
|
||||
pendingBadgeCount.value = 0;
|
||||
return;
|
||||
}
|
||||
const expectedTab = resolveTabNameByPath(path);
|
||||
const fallbackTab = visibleTabs.value[0];
|
||||
const queryTab = String(route.query.tab || '');
|
||||
|
||||
// 进入审批管理根路径时,默认落到当前可见页签中排序第一个的页签。
|
||||
if (route.path === '/sys/approval' && !queryTab && fallbackTab) {
|
||||
activeTab.value = fallbackTab.name;
|
||||
if (fallbackTab.name !== 'flow') {
|
||||
await router.replace({
|
||||
path: '/sys/approval',
|
||||
query: { tab: fallbackTab.name },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const expectedTab = resolveTabNameByRoute();
|
||||
if (visibleTabNames.value.includes(expectedTab)) {
|
||||
activeTab.value = expectedTab;
|
||||
} else {
|
||||
if (fallbackTab && path !== fallbackTab.path) {
|
||||
await router.replace(fallbackTab.path);
|
||||
const fallbackQuery =
|
||||
fallbackTab?.name === 'flow' ? {} : { tab: fallbackTab?.name };
|
||||
const currentQueryTab = String(route.query.tab || '');
|
||||
if (
|
||||
fallbackTab &&
|
||||
(route.path !== '/sys/approval' || currentQueryTab !== String(fallbackTab.name))
|
||||
) {
|
||||
await router.replace({
|
||||
path: '/sys/approval',
|
||||
query: fallbackQuery,
|
||||
});
|
||||
return;
|
||||
}
|
||||
activeTab.value = fallbackTab?.name || 'flow';
|
||||
@@ -206,10 +257,16 @@ function handleTabChange(name: number | string) {
|
||||
return;
|
||||
}
|
||||
const target = visibleTabs.value.find((item) => item.name === name);
|
||||
if (!target || route.path === target.path) {
|
||||
const nextQuery = name === 'flow' ? {} : { tab: name };
|
||||
const currentQueryTab = String(route.query.tab || '');
|
||||
const currentTab = currentQueryTab || 'flow';
|
||||
if (!target || currentTab === name) {
|
||||
return;
|
||||
}
|
||||
router.push(target.path);
|
||||
router.replace({
|
||||
path: '/sys/approval',
|
||||
query: nextQuery,
|
||||
});
|
||||
}
|
||||
|
||||
function reloadCurrentTab() {
|
||||
|
||||
@@ -611,6 +611,9 @@ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
|
||||
meta: { fullPathKey } = {},
|
||||
query = {},
|
||||
} = tab as RouteLocationNormalized;
|
||||
if (path === '/sys/approval') {
|
||||
return path;
|
||||
}
|
||||
// pageKey可能是数组(查询参数重复时可能出现)
|
||||
const pageKey = Array.isArray(query.pageKey)
|
||||
? query.pageKey[0]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
interface BotInfo {
|
||||
alias: string;
|
||||
anonymousEnabled: boolean;
|
||||
approvalPending?: boolean;
|
||||
currentApprovalInstanceId?: number | string;
|
||||
currentApprovalActionType?: string;
|
||||
displayPublishStatus?: string;
|
||||
created: string;
|
||||
createdBy: number;
|
||||
deptId: number;
|
||||
|
||||
Reference in New Issue
Block a user