feat: 完成分享、单会话与发布审批改造
- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈 - Web 新登录替换旧会话,并保持 API Key 会话隔离 - 发布审批增加必填说明并在审批详情展示 - 账号重置与导入改用可配置默认强密码
This commit is contained in:
@@ -307,16 +307,38 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手发布审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
botPublishAppService.submitPublishApproval(id),
|
||||
botPublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检聊天助手发布是否命中审批流。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(botPublishAppService.isPublishApprovalRequired(id));
|
||||
}
|
||||
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
|
||||
@@ -203,18 +203,34 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitPublishApproval(id),
|
||||
knowledgePublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检知识库发布是否命中审批流。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.isPublishApprovalRequired(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
|
||||
@@ -8,11 +8,13 @@ import com.mybatisflex.core.paginate.Page;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.permission.WorkflowShareResourceAccessGrantProvider;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
@@ -53,6 +55,7 @@ import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -94,6 +97,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private WorkflowShareResourceAccessGrantProvider workflowShareGrantProvider;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
@@ -256,18 +261,48 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限发布工作流"
|
||||
)
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitPublishApproval(id),
|
||||
workflowPublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检工作流发布是否命中审批流。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限发布工作流"
|
||||
)
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.isPublishApprovalRequired(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
@@ -276,6 +311,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限下线工作流"
|
||||
)
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitOfflineApproval(id),
|
||||
@@ -311,6 +353,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/remove")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限删除工作流"
|
||||
)
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitDeleteApproval(id),
|
||||
@@ -378,14 +427,68 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
workflow.setId(null);
|
||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||
workflow.setRevision(0);
|
||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||
service.save(workflow);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作流;分享访问只允许按修订号更新设计内容。
|
||||
*
|
||||
* @param entity 工作流更新内容
|
||||
* @return 更新结果
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> update(@JsonBody Workflow entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
throw new BusinessException("工作流 ID 不能为空");
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
boolean sharedRequest = workflowShareGrantProvider.isSharedRequestFor(
|
||||
entity.getId(),
|
||||
loginAccount
|
||||
);
|
||||
if (sharedRequest && entity.getContent() == null) {
|
||||
throw new BusinessException(403, 403, "分享链接仅允许编辑工作流内容");
|
||||
}
|
||||
if (entity.getContent() == null) {
|
||||
return super.update(entity);
|
||||
}
|
||||
if (entity.getRevision() == null) {
|
||||
throw workflowRevisionConflict();
|
||||
}
|
||||
Result<?> beforeResult = onSaveOrUpdateBefore(entity, false);
|
||||
if (beforeResult != null) {
|
||||
return beforeResult;
|
||||
}
|
||||
boolean updated = service.updateContentByRevision(
|
||||
entity.getId(),
|
||||
entity.getContent(),
|
||||
entity.getRevision(),
|
||||
new Date(),
|
||||
loginAccount.getId()
|
||||
);
|
||||
if (!updated) {
|
||||
throw workflowRevisionConflict();
|
||||
}
|
||||
entity.setRevision(entity.getRevision() + 1);
|
||||
if (!sharedRequest) {
|
||||
entity.setContent(null);
|
||||
service.updateById(entity);
|
||||
}
|
||||
onSaveOrUpdateAfter(entity, false);
|
||||
return Result.ok(Map.of("revision", entity.getRevision()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
if (isSave && entity.getRevision() == null) {
|
||||
entity.setRevision(0);
|
||||
}
|
||||
if (!isSave && entity.getId() != null) {
|
||||
Workflow existed = requireWorkflow(String.valueOf(entity.getId()));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, existed, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
@@ -485,6 +588,15 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作流内容修订冲突异常。
|
||||
*
|
||||
* @return HTTP 409 业务异常
|
||||
*/
|
||||
private BusinessException workflowRevisionConflict() {
|
||||
return new BusinessException(409, 409, "工作流已被其他人更新,请刷新后重新编辑");
|
||||
}
|
||||
|
||||
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowShareService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.ai.vo.WorkflowShareCreateResult;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流协作分享管理接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/workflowShare")
|
||||
public class WorkflowShareController {
|
||||
|
||||
@Resource
|
||||
private WorkflowShareService workflowShareService;
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
@Resource
|
||||
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
|
||||
/**
|
||||
* 创建或刷新工作流协作分享链接。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 分享创建结果
|
||||
*/
|
||||
@PostMapping("/url/create")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<WorkflowShareCreateResult> createUrlShare(
|
||||
HttpServletRequest request,
|
||||
@JsonBody("workflowId") BigInteger workflowId
|
||||
) {
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.MANAGE,
|
||||
"无权限分享工作流"
|
||||
);
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
WorkflowShareCreateResult result = workflowShareService.createUrlShare(
|
||||
workflowId,
|
||||
loginAccount.getTenantId(),
|
||||
loginAccount.getDeptId(),
|
||||
loginAccount.getId(),
|
||||
buildShareBaseUrl(request)
|
||||
);
|
||||
knowledgeShareAuditService.log(
|
||||
loginAccount.getId(),
|
||||
"创建工作流协作分享",
|
||||
"WORKFLOW_SHARE_CREATE",
|
||||
request.getRequestURI(),
|
||||
Map.of("workflowId", workflowId, "shareId", result.getId())
|
||||
);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前 URL 分享指向的工作流。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 工作流标识
|
||||
*/
|
||||
@GetMapping("/resolve")
|
||||
public Result<Map<String, BigInteger>> resolveUrlShare(HttpServletRequest request) {
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
WorkflowShare share = workflowShareService.resolveUrlShare(
|
||||
request.getHeader(WorkflowSharePolicy.SHARE_KEY_HEADER),
|
||||
loginAccount.getTenantId()
|
||||
);
|
||||
return Result.ok(Map.of("workflowId", share.getWorkflowId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理端来源构建工作流分享基础 URL。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 工作流设计页 URL
|
||||
*/
|
||||
private String buildShareBaseUrl(HttpServletRequest request) {
|
||||
String refererBaseUrl = extractFrontendBaseUrl(RequestUtil.getReferer(request));
|
||||
if (refererBaseUrl != null) {
|
||||
return refererBaseUrl + "/ai/workflow/design";
|
||||
}
|
||||
|
||||
String forwardedOrigin = buildForwardedOrigin(request);
|
||||
if (forwardedOrigin != null) {
|
||||
return forwardedOrigin
|
||||
+ normalizeBasePath(firstHeaderValue(request.getHeader("X-Forwarded-Prefix")))
|
||||
+ "/ai/workflow/design";
|
||||
}
|
||||
|
||||
String origin = normalizeOrigin(request.getHeader("Origin"));
|
||||
if (origin != null) {
|
||||
return origin + normalizeBasePath(request.getContextPath()) + "/ai/workflow/design";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(request.getScheme()).append("://").append(request.getServerName());
|
||||
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
|
||||
builder.append(':').append(request.getServerPort());
|
||||
}
|
||||
return builder.append(normalizeBasePath(request.getContextPath()))
|
||||
.append("/ai/workflow/design")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从来源地址提取前端 origin 与部署基路径。
|
||||
*
|
||||
* @param sourceUrl 来源地址
|
||||
* @return origin 与部署基路径,无法解析时返回 {@code null}
|
||||
*/
|
||||
private String extractFrontendBaseUrl(String sourceUrl) {
|
||||
if (sourceUrl == null || sourceUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(sourceUrl.trim());
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
String origin = extractOrigin(sourceUrl);
|
||||
return origin == null ? null : origin + inferFrontendBasePath(uri.getPath());
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从前端页面路径推断部署基路径。
|
||||
*
|
||||
* @param path 页面路径
|
||||
* @return 规范化后的部署基路径
|
||||
*/
|
||||
private String inferFrontendBasePath(String path) {
|
||||
if (path == null || path.isBlank() || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
for (String marker : new String[]{"/ai/", "/auth/", "/share/"}) {
|
||||
int markerIndex = path.indexOf(marker);
|
||||
if (markerIndex > 0) {
|
||||
return normalizeBasePath(path.substring(0, markerIndex));
|
||||
}
|
||||
if (markerIndex == 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化部署基路径。
|
||||
*
|
||||
* @param basePath 原始基路径
|
||||
* @return 无尾斜杠的基路径
|
||||
*/
|
||||
private String normalizeBasePath(String basePath) {
|
||||
if (basePath == null || basePath.isBlank() || "/".equals(basePath.trim())) {
|
||||
return "";
|
||||
}
|
||||
String normalized = basePath.trim();
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = "/" + normalized;
|
||||
}
|
||||
while (normalized.endsWith("/") && normalized.length() > 1) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按反向代理头构建外部访问 origin。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 外部 origin,缺少代理头时返回 {@code null}
|
||||
*/
|
||||
private String buildForwardedOrigin(HttpServletRequest request) {
|
||||
String proto = firstHeaderValue(request.getHeader("X-Forwarded-Proto"));
|
||||
String host = firstHeaderValue(request.getHeader("X-Forwarded-Host"));
|
||||
if (proto == null || host == null) {
|
||||
return null;
|
||||
}
|
||||
return normalizeOrigin(proto + "://" + host);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 提取 origin。
|
||||
*
|
||||
* @param url 完整 URL
|
||||
* @return origin,无法解析时返回 {@code null}
|
||||
*/
|
||||
private String extractOrigin(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(url.trim());
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||
if (uri.getPort() != -1 && uri.getPort() != 80 && uri.getPort() != 443) {
|
||||
builder.append(':').append(uri.getPort());
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 origin。
|
||||
*
|
||||
* @param origin 原始 origin
|
||||
* @return 规范化结果
|
||||
*/
|
||||
private String normalizeOrigin(String origin) {
|
||||
return extractOrigin(origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取代理头的首个有效值。
|
||||
*
|
||||
* @param value 原始请求头
|
||||
* @return 首个有效值
|
||||
*/
|
||||
private String firstHeaderValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
int commaIndex = value.indexOf(',');
|
||||
String normalized = commaIndex >= 0 ? value.substring(0, commaIndex) : value;
|
||||
normalized = normalized.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link WorkflowShareController} 分享地址构建测试。
|
||||
*/
|
||||
public class WorkflowShareControllerTest {
|
||||
|
||||
/**
|
||||
* 验证分享地址保留前端部署基路径。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreserveFrontendBasePathFromReferer() throws Exception {
|
||||
HttpServletRequest request = request(Map.of(
|
||||
"referer",
|
||||
"https://example.test/easyflow/ai/workflow?page=1"
|
||||
));
|
||||
|
||||
Assert.assertEquals(
|
||||
buildShareBaseUrl(request),
|
||||
"https://example.test/easyflow/ai/workflow/design"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证反向代理头用于构建外部 HTTPS 分享地址。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseForwardedOriginAndPrefix() throws Exception {
|
||||
HttpServletRequest request = request(Map.of(
|
||||
"x-forwarded-proto", "https",
|
||||
"x-forwarded-host", "example.test",
|
||||
"x-forwarded-prefix", "/easyflow"
|
||||
));
|
||||
|
||||
Assert.assertEquals(
|
||||
buildShareBaseUrl(request),
|
||||
"https://example.test/easyflow/ai/workflow/design"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用控制器的分享基础地址构建方法。
|
||||
*
|
||||
* @param request 模拟 HTTP 请求
|
||||
* @return 分享基础地址
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
private String buildShareBaseUrl(HttpServletRequest request) throws Exception {
|
||||
Method method = WorkflowShareController.class.getDeclaredMethod(
|
||||
"buildShareBaseUrl",
|
||||
HttpServletRequest.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(new WorkflowShareController(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅提供请求头能力的轻量 Servlet 请求代理。
|
||||
*
|
||||
* @param headers 小写请求头映射
|
||||
* @return HTTP 请求代理
|
||||
*/
|
||||
private HttpServletRequest request(Map<String, String> headers) {
|
||||
return (HttpServletRequest) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{HttpServletRequest.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("getHeader".equals(method.getName())) {
|
||||
String name = String.valueOf(args[0]).toLowerCase(Locale.ROOT);
|
||||
return headers.get(name);
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回代理方法所需的基础类型默认值。
|
||||
*
|
||||
* @param returnType 返回值类型
|
||||
* @return 对应默认值
|
||||
*/
|
||||
private Object defaultValue(Class<?> returnType) {
|
||||
if (!returnType.isPrimitive()) {
|
||||
return null;
|
||||
}
|
||||
if (boolean.class == returnType) {
|
||||
return false;
|
||||
}
|
||||
if (char.class == returnType) {
|
||||
return '\0';
|
||||
}
|
||||
if (byte.class == returnType) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (short.class == returnType) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (int.class == returnType) {
|
||||
return 0;
|
||||
}
|
||||
if (long.class == returnType) {
|
||||
return 0L;
|
||||
}
|
||||
if (float.class == returnType) {
|
||||
return 0F;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user