feat: 完成分享、单会话与发布审批改造
- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈 - Web 新登录替换旧会话,并保持 API Key 会话隔离 - 发布审批增加必填说明并在审批详情展示 - 账号重置与导入改用可配置默认强密码
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 工作流协作分享记录。
|
||||
*/
|
||||
@Table("tb_workflow_share")
|
||||
public class WorkflowShare implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "ID")
|
||||
private BigInteger id;
|
||||
|
||||
@Column(comment = "工作流ID")
|
||||
private BigInteger workflowId;
|
||||
|
||||
@Column(comment = "分享密钥哈希")
|
||||
private String shareKeyHash;
|
||||
|
||||
@Column(comment = "分享状态")
|
||||
private String status;
|
||||
|
||||
@Column(comment = "过期时间")
|
||||
private Date expiresAt;
|
||||
|
||||
@Column(tenantId = true, comment = "租户ID")
|
||||
private BigInteger tenantId;
|
||||
|
||||
@Column(comment = "部门ID")
|
||||
private BigInteger deptId;
|
||||
|
||||
@Column(comment = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Column(comment = "创建人")
|
||||
private BigInteger createdBy;
|
||||
|
||||
@Column(comment = "修改时间")
|
||||
private Date modified;
|
||||
|
||||
@Column(comment = "修改人")
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
/**
|
||||
* 获取记录 ID。
|
||||
*
|
||||
* @return 记录 ID
|
||||
*/
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置记录 ID。
|
||||
*
|
||||
* @param id 记录 ID
|
||||
*/
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流 ID。
|
||||
*
|
||||
* @return 工作流 ID
|
||||
*/
|
||||
public BigInteger getWorkflowId() {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作流 ID。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
*/
|
||||
public void setWorkflowId(BigInteger workflowId) {
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享密钥哈希。
|
||||
*
|
||||
* @return 分享密钥哈希
|
||||
*/
|
||||
public String getShareKeyHash() {
|
||||
return shareKeyHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分享密钥哈希。
|
||||
*
|
||||
* @param shareKeyHash 分享密钥哈希
|
||||
*/
|
||||
public void setShareKeyHash(String shareKeyHash) {
|
||||
this.shareKeyHash = shareKeyHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享状态。
|
||||
*
|
||||
* @return 分享状态
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分享状态。
|
||||
*
|
||||
* @param status 分享状态
|
||||
*/
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间。
|
||||
*
|
||||
* @return 过期时间
|
||||
*/
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间。
|
||||
*
|
||||
* @param expiresAt 过期时间
|
||||
*/
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户 ID。
|
||||
*
|
||||
* @return 租户 ID
|
||||
*/
|
||||
public BigInteger getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置租户 ID。
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
*/
|
||||
public void setTenantId(BigInteger tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门 ID。
|
||||
*
|
||||
* @return 部门 ID
|
||||
*/
|
||||
public BigInteger getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置部门 ID。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
*/
|
||||
public void setDeptId(BigInteger deptId) {
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建时间。
|
||||
*
|
||||
* @return 创建时间
|
||||
*/
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建时间。
|
||||
*
|
||||
* @param created 创建时间
|
||||
*/
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建人。
|
||||
*
|
||||
* @return 创建人账号 ID
|
||||
*/
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建人。
|
||||
*
|
||||
* @param createdBy 创建人账号 ID
|
||||
*/
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取修改时间。
|
||||
*
|
||||
* @return 修改时间
|
||||
*/
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置修改时间。
|
||||
*
|
||||
* @param modified 修改时间
|
||||
*/
|
||||
public void setModified(Date modified) {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取修改人。
|
||||
*
|
||||
* @return 修改人账号 ID
|
||||
*/
|
||||
public BigInteger getModifiedBy() {
|
||||
return modifiedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置修改人。
|
||||
*
|
||||
* @param modifiedBy 修改人账号 ID
|
||||
*/
|
||||
public void setModifiedBy(BigInteger modifiedBy) {
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "工作流设计的 JSON 内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 工作流内容修订号
|
||||
*/
|
||||
@Column(comment = "工作流内容修订号")
|
||||
private Integer revision;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@@ -205,6 +211,24 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流内容修订号。
|
||||
*
|
||||
* @return 工作流内容修订号
|
||||
*/
|
||||
public Integer getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作流内容修订号。
|
||||
*
|
||||
* @param revision 工作流内容修订号
|
||||
*/
|
||||
public void setRevision(Integer revision) {
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 映射层。
|
||||
@@ -11,4 +16,24 @@ import com.mybatisflex.core.BaseMapper;
|
||||
*/
|
||||
public interface WorkflowMapper extends BaseMapper<Workflow> {
|
||||
|
||||
/**
|
||||
* 按预期修订号原子更新工作流内容。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param content 工作流内容
|
||||
* @param expectedRevision 预期修订号
|
||||
* @param modified 修改时间
|
||||
* @param modifiedBy 修改人账号 ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE tb_workflow "
|
||||
+ "SET content=#{content}, revision=revision+1, modified=#{modified}, modified_by=#{modifiedBy} "
|
||||
+ "WHERE id=#{id} AND revision=#{expectedRevision}")
|
||||
int updateContentByRevision(
|
||||
@Param("id") BigInteger id,
|
||||
@Param("content") String content,
|
||||
@Param("expectedRevision") Integer expectedRevision,
|
||||
@Param("modified") Date modified,
|
||||
@Param("modifiedBy") BigInteger modifiedBy
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
|
||||
/**
|
||||
* 工作流协作分享记录映射层。
|
||||
*/
|
||||
public interface WorkflowShareMapper extends BaseMapper<WorkflowShare> {
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.WorkflowShareService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessGrantProvider;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 基于工作流分享密钥补充协作编辑权限。
|
||||
*/
|
||||
@Component
|
||||
public class WorkflowShareResourceAccessGrantProvider implements ResourceAccessGrantProvider {
|
||||
|
||||
private static final String AUDIT_RECORDED_ATTRIBUTE =
|
||||
WorkflowShareResourceAccessGrantProvider.class.getName() + ".AUDIT_RECORDED";
|
||||
|
||||
private final WorkflowShareService workflowShareService;
|
||||
private final KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
|
||||
/**
|
||||
* 创建工作流分享授权提供器。
|
||||
*
|
||||
* @param workflowShareService 工作流分享服务
|
||||
* @param knowledgeShareAuditService 分享审计服务
|
||||
*/
|
||||
public WorkflowShareResourceAccessGrantProvider(
|
||||
WorkflowShareService workflowShareService,
|
||||
KnowledgeShareAuditService knowledgeShareAuditService
|
||||
) {
|
||||
this.workflowShareService = workflowShareService;
|
||||
this.knowledgeShareAuditService = knowledgeShareAuditService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean grants(
|
||||
LoginAccount loginAccount,
|
||||
CategoryResourceType resourceType,
|
||||
VisibilityResource resource,
|
||||
ResourceAction action
|
||||
) {
|
||||
if (resourceType != CategoryResourceType.WORKFLOW
|
||||
|| !(resource instanceof Workflow workflow)
|
||||
|| loginAccount == null
|
||||
|| !WorkflowSharePolicy.isAllowedRequest(
|
||||
currentRequestMethod(),
|
||||
currentRequestUri(),
|
||||
action
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
return isSharedRequestFor(workflow.getId(), loginAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否携带指定工作流的有效分享密钥。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param loginAccount 当前登录账号
|
||||
* @return 未携带分享密钥时返回 {@code false},携带有效密钥时返回 {@code true}
|
||||
*/
|
||||
public boolean isSharedRequestFor(BigInteger workflowId, LoginAccount loginAccount) {
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request == null) {
|
||||
return false;
|
||||
}
|
||||
String shareKey = request.getHeader(WorkflowSharePolicy.SHARE_KEY_HEADER);
|
||||
if (shareKey == null || shareKey.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
WorkflowShare share = workflowShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
workflowId,
|
||||
loginAccount == null ? null : loginAccount.getTenantId()
|
||||
);
|
||||
recordSharedOperation(request, share, loginAccount);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个请求只记录一次工作流分享操作审计。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param share 分享记录
|
||||
* @param loginAccount 当前登录账号
|
||||
*/
|
||||
private void recordSharedOperation(
|
||||
HttpServletRequest request,
|
||||
WorkflowShare share,
|
||||
LoginAccount loginAccount
|
||||
) {
|
||||
if (Boolean.TRUE.equals(request.getAttribute(AUDIT_RECORDED_ATTRIBUTE))) {
|
||||
return;
|
||||
}
|
||||
request.setAttribute(AUDIT_RECORDED_ATTRIBUTE, Boolean.TRUE);
|
||||
knowledgeShareAuditService.log(
|
||||
loginAccount.getId(),
|
||||
"使用工作流协作分享",
|
||||
"WORKFLOW_SHARE_ACCESS",
|
||||
request.getRequestURI(),
|
||||
Map.of(
|
||||
"workflowId", share.getWorkflowId(),
|
||||
"shareId", share.getId(),
|
||||
"method", request.getMethod()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 HTTP 请求。
|
||||
*
|
||||
* @return 当前请求,不在 Web 请求中时返回 {@code null}
|
||||
*/
|
||||
private HttpServletRequest currentRequest() {
|
||||
if (!(RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes)) {
|
||||
return null;
|
||||
}
|
||||
return attributes.getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求方法。
|
||||
*
|
||||
* @return 请求方法
|
||||
*/
|
||||
private String currentRequestMethod() {
|
||||
HttpServletRequest request = currentRequest();
|
||||
return request == null ? null : request.getMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求路径。
|
||||
*
|
||||
* @return 请求路径
|
||||
*/
|
||||
private String currentRequestUri() {
|
||||
HttpServletRequest request = currentRequest();
|
||||
return request == null ? null : request.getRequestURI();
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,38 @@ public interface AiResourceLifecycleService {
|
||||
* @return 执行结果
|
||||
*/
|
||||
ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 提交带发起说明的资源动作。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 执行结果
|
||||
*/
|
||||
ApprovalActionResult submitAction(
|
||||
String resourceType,
|
||||
BigInteger resourceId,
|
||||
String actionType,
|
||||
BigInteger operatorId,
|
||||
String applicationReason
|
||||
);
|
||||
|
||||
/**
|
||||
* 预检当前资源动作是否命中审批流。
|
||||
*
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param actionType 动作类型
|
||||
* @param operatorId 操作人 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
boolean isApprovalRequired(
|
||||
String resourceType,
|
||||
BigInteger resourceId,
|
||||
String actionType,
|
||||
BigInteger operatorId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.approval.support.ApprovalApplicationReasonPolicy;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -44,6 +45,21 @@ public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleServic
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||
return submitAction(resourceType, resourceId, actionType, operatorId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApprovalActionResult submitAction(
|
||||
String resourceType,
|
||||
BigInteger resourceId,
|
||||
String actionType,
|
||||
BigInteger operatorId,
|
||||
String applicationReason
|
||||
) {
|
||||
AiResourceLifecycleHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||
@@ -51,6 +67,11 @@ public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleServic
|
||||
handler.applyApprovedAction(actionType, resourceId, readResourceSnapshot(request.getSnapshotJson()), operatorId);
|
||||
return ApprovalActionResult.direct();
|
||||
}
|
||||
request.setApplicationReason(ApprovalApplicationReasonPolicy.normalize(
|
||||
true,
|
||||
actionType,
|
||||
applicationReason
|
||||
));
|
||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||
handler.updatePendingState(
|
||||
resourceId,
|
||||
@@ -60,6 +81,21 @@ public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleServic
|
||||
return ApprovalActionResult.required(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isApprovalRequired(
|
||||
String resourceType,
|
||||
BigInteger resourceId,
|
||||
String actionType,
|
||||
BigInteger operatorId
|
||||
) {
|
||||
AiResourceLifecycleHandler handler = getHandler(resourceType);
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||
return approvalMatchService.matchFlowOrNull(request) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
||||
@@ -25,11 +25,29 @@ public class BotPublishAppService {
|
||||
* 提交聊天助手发布审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id, String applicationReason) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId(),
|
||||
applicationReason
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检聊天助手发布是否需要审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
public boolean isPublishApprovalRequired(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.isApprovalRequired(
|
||||
ApprovalResourceType.BOT.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
|
||||
@@ -36,11 +36,29 @@ public class KnowledgePublishAppService {
|
||||
* 提交知识库发布审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id, String applicationReason) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId(),
|
||||
applicationReason
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检知识库发布是否需要审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
public boolean isPublishApprovalRequired(BigInteger id) {
|
||||
assertId(id);
|
||||
return aiResourceLifecycleService.isApprovalRequired(
|
||||
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
|
||||
@@ -36,11 +36,29 @@ public class WorkflowPublishAppService {
|
||||
* 提交工作流发布审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 动作执行结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id, String applicationReason) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId(),
|
||||
applicationReason
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检工作流发布是否需要审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
public boolean isPublishApprovalRequired(BigInteger id) {
|
||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
||||
return aiResourceLifecycleService.isApprovalRequired(
|
||||
ApprovalResourceType.WORKFLOW.getCode(),
|
||||
id,
|
||||
ApprovalActionType.PUBLISH.getCode(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import tech.easyflow.ai.entity.Workflow;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 服务层。
|
||||
@@ -44,4 +45,22 @@ public interface WorkflowService extends IService<Workflow> {
|
||||
* @return 已发布视图
|
||||
*/
|
||||
Workflow toPublishedView(Workflow workflow);
|
||||
|
||||
/**
|
||||
* 按预期修订号原子更新工作流内容。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param content 工作流内容
|
||||
* @param expectedRevision 预期修订号
|
||||
* @param modified 修改时间
|
||||
* @param modifiedBy 修改人账号 ID
|
||||
* @return 更新成功时返回 {@code true}
|
||||
*/
|
||||
boolean updateContentByRevision(
|
||||
BigInteger id,
|
||||
String content,
|
||||
Integer expectedRevision,
|
||||
Date modified,
|
||||
BigInteger modifiedBy
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
import tech.easyflow.ai.vo.WorkflowShareCreateResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作流协作分享服务。
|
||||
*/
|
||||
public interface WorkflowShareService extends IService<WorkflowShare> {
|
||||
|
||||
/**
|
||||
* 创建或刷新工作流的唯一协作分享链接。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param deptId 部门 ID
|
||||
* @param operatorId 操作人账号 ID
|
||||
* @param baseUrl 工作流分享基础 URL
|
||||
* @return 创建结果
|
||||
*/
|
||||
WorkflowShareCreateResult createUrlShare(
|
||||
BigInteger workflowId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验分享密钥是否可访问指定工作流。
|
||||
*
|
||||
* @param shareKey 原始分享密钥
|
||||
* @param workflowId 工作流 ID
|
||||
* @param tenantId 当前登录租户 ID
|
||||
* @return 有效分享记录
|
||||
*/
|
||||
WorkflowShare assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger workflowId,
|
||||
BigInteger tenantId
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验分享密钥并解析目标工作流。
|
||||
*
|
||||
* @param shareKey 原始分享密钥
|
||||
* @param tenantId 当前登录租户 ID
|
||||
* @return 有效分享记录
|
||||
*/
|
||||
WorkflowShare resolveUrlShare(String shareKey, BigInteger tenantId);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.ai.utils.CustomBeanUtils;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -115,7 +116,11 @@ public class WorkflowServiceImpl extends ServiceImpl<WorkflowMapper, Workflow> i
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
|
||||
int nextRevision = workFlow.getRevision() == null ? 1 : workFlow.getRevision() + 1;
|
||||
CustomBeanUtils.copyPropertiesIgnoreNull(entity,workFlow);
|
||||
if (entity.getContent() != null) {
|
||||
workFlow.setRevision(nextRevision);
|
||||
}
|
||||
|
||||
if ("".equals(workFlow.getAlias())){
|
||||
workFlow.setAlias(null);
|
||||
@@ -125,5 +130,24 @@ public class WorkflowServiceImpl extends ServiceImpl<WorkflowMapper, Workflow> i
|
||||
return super.updateById(workFlow,false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean updateContentByRevision(
|
||||
BigInteger id,
|
||||
String content,
|
||||
Integer expectedRevision,
|
||||
Date modified,
|
||||
BigInteger modifiedBy
|
||||
) {
|
||||
return getMapper().updateContentByRevision(
|
||||
id,
|
||||
content,
|
||||
expectedRevision,
|
||||
modified,
|
||||
modifiedBy
|
||||
) == 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareStatus;
|
||||
import tech.easyflow.ai.mapper.WorkflowShareMapper;
|
||||
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.cache.RedisLockExecutor;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 工作流协作分享服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowShareServiceImpl extends ServiceImpl<WorkflowShareMapper, WorkflowShare>
|
||||
implements WorkflowShareService {
|
||||
|
||||
private static final String LOCK_KEY_PREFIX = "easyflow:lock:workflow-share:";
|
||||
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Resource
|
||||
private RedisLockExecutor redisLockExecutor;
|
||||
|
||||
@Resource
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public WorkflowShareCreateResult createUrlShare(
|
||||
BigInteger workflowId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl
|
||||
) {
|
||||
return redisLockExecutor.executeWithLock(
|
||||
LOCK_KEY_PREFIX + workflowId,
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
// 在释放分布式锁前完成事务提交,避免并发请求观察到未提交的旧分享状态。
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
|
||||
return transactionTemplate.execute(status -> createOrReplaceShare(
|
||||
workflowId,
|
||||
tenantId,
|
||||
deptId,
|
||||
operatorId,
|
||||
baseUrl
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public WorkflowShare assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger workflowId,
|
||||
BigInteger tenantId
|
||||
) {
|
||||
if (workflowId == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
WorkflowShare share = resolveUrlShare(shareKey, tenantId);
|
||||
if (!workflowId.equals(share.getWorkflowId())) {
|
||||
throw invalidShare();
|
||||
}
|
||||
return share;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public WorkflowShare resolveUrlShare(String shareKey, BigInteger tenantId) {
|
||||
if (shareKey == null || shareKey.isBlank() || tenantId == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
WorkflowShare share = getOne(QueryWrapper.create()
|
||||
.eq(WorkflowShare::getShareKeyHash, WorkflowSharePolicy.hashShareKey(shareKey))
|
||||
.eq(WorkflowShare::getStatus, KnowledgeShareStatus.ENABLED.name()));
|
||||
if (share == null || !tenantId.equals(share.getTenantId())) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (share.getExpiresAt() == null || !share.getExpiresAt().after(new Date())) {
|
||||
throw new BusinessException(403, 403, "工作流分享链接已过期");
|
||||
}
|
||||
Workflow workflow = workflowService.getById(share.getWorkflowId());
|
||||
if (workflow == null || !tenantId.equals(workflow.getTenantId())) {
|
||||
throw invalidShare();
|
||||
}
|
||||
return share;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在锁保护下创建或替换工作流的唯一分享记录。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param deptId 部门 ID
|
||||
* @param operatorId 操作人账号 ID
|
||||
* @param baseUrl 工作流分享基础 URL
|
||||
* @return 创建结果
|
||||
*/
|
||||
private WorkflowShareCreateResult createOrReplaceShare(
|
||||
BigInteger workflowId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl
|
||||
) {
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null || tenantId == null || !tenantId.equals(workflow.getTenantId())) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
|
||||
String shareKey = UUID.randomUUID().toString().replace("-", "");
|
||||
Date now = new Date();
|
||||
Date expiresAt = WorkflowSharePolicy.defaultExpiresAt(now);
|
||||
invalidateExistingShares(workflowId, operatorId, now);
|
||||
WorkflowShare share = new WorkflowShare();
|
||||
share.setWorkflowId(workflowId);
|
||||
share.setTenantId(tenantId);
|
||||
share.setDeptId(deptId);
|
||||
share.setShareKeyHash(WorkflowSharePolicy.hashShareKey(shareKey));
|
||||
share.setStatus(KnowledgeShareStatus.ENABLED.name());
|
||||
share.setExpiresAt(expiresAt);
|
||||
share.setCreated(now);
|
||||
share.setCreatedBy(operatorId);
|
||||
share.setModified(now);
|
||||
share.setModifiedBy(operatorId);
|
||||
save(share);
|
||||
|
||||
WorkflowShareCreateResult result = new WorkflowShareCreateResult();
|
||||
result.setId(share.getId());
|
||||
result.setShareKey(shareKey);
|
||||
result.setShareUrl(buildShareUrl(baseUrl, shareKey));
|
||||
result.setExpiresAt(expiresAt);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使工作流已有的有效分享记录失效。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param operatorId 操作人账号 ID
|
||||
* @param now 当前时间
|
||||
*/
|
||||
private void invalidateExistingShares(BigInteger workflowId, BigInteger operatorId, Date now) {
|
||||
List<WorkflowShare> activeShares = list(QueryWrapper.create()
|
||||
.eq(WorkflowShare::getWorkflowId, workflowId)
|
||||
.eq(WorkflowShare::getStatus, KnowledgeShareStatus.ENABLED.name()));
|
||||
for (WorkflowShare activeShare : activeShares) {
|
||||
WorkflowShare update = new WorkflowShare();
|
||||
update.setId(activeShare.getId());
|
||||
update.setStatus(KnowledgeShareStatus.DISABLED.name());
|
||||
update.setModified(now);
|
||||
update.setModifiedBy(operatorId);
|
||||
updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建仅包含分享密钥的协作 URL。
|
||||
*
|
||||
* @param baseUrl 分享基础 URL
|
||||
* @param shareKey 原始分享密钥
|
||||
* @return 分享 URL
|
||||
*/
|
||||
private String buildShareUrl(String baseUrl, String shareKey) {
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return baseUrl + (baseUrl.contains("?") ? "&" : "?") + "shareKey=" + shareKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建无效分享异常。
|
||||
*
|
||||
* @return 无效分享异常
|
||||
*/
|
||||
private BusinessException invalidShare() {
|
||||
return new BusinessException(403, 403, "工作流分享链接无效");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package tech.easyflow.ai.share;
|
||||
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作流协作分享的密钥、时效与接口授权策略。
|
||||
*/
|
||||
public final class WorkflowSharePolicy {
|
||||
|
||||
/**
|
||||
* 工作流协作分享请求头。
|
||||
*/
|
||||
public static final String SHARE_KEY_HEADER = "X-Workflow-Share-Key";
|
||||
|
||||
private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(30);
|
||||
private static final Set<String> ALLOWED_REQUESTS = Set.of(
|
||||
permissionKey("GET", "/api/v1/workflow/detail", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/getRunningParameters", ResourceAction.READ),
|
||||
permissionKey("POST", "/api/v1/workflow/update", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/check", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/singleRun", ResourceAction.USE),
|
||||
permissionKey("POST", "/api/v1/workflow/runAsync", ResourceAction.USE),
|
||||
permissionKey("POST", "/api/v1/workflow/getChainStatus", ResourceAction.USE),
|
||||
permissionKey("POST", "/api/v1/workflow/resume", ResourceAction.USE),
|
||||
permissionKey("GET", "/api/v1/workflow/publishApprovalRequirement", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/submitPublishApproval", ResourceAction.MANAGE)
|
||||
);
|
||||
|
||||
private WorkflowSharePolicy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算分享密钥的 SHA-256 摘要。
|
||||
*
|
||||
* @param shareKey 原始分享密钥
|
||||
* @return 十六进制摘要
|
||||
*/
|
||||
public static String hashShareKey(String shareKey) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] value = digest.digest(shareKey.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(value);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算默认过期时间。
|
||||
*
|
||||
* @param createdAt 创建时间
|
||||
* @return 创建后 30 分钟的时间
|
||||
*/
|
||||
public static Date defaultExpiresAt(Date createdAt) {
|
||||
if (createdAt == null) {
|
||||
throw new IllegalArgumentException("创建时间不能为空");
|
||||
}
|
||||
return new Date(createdAt.getTime() + DEFAULT_EXPIRE_DURATION.toMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 HTTP 请求是否位于工作流协作授权白名单。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @param requestUri 请求路径
|
||||
* @param action 资源动作
|
||||
* @return 位于白名单时返回 {@code true}
|
||||
*/
|
||||
public static boolean isAllowedRequest(String method, String requestUri, ResourceAction action) {
|
||||
if (method == null || requestUri == null || action == null) {
|
||||
return false;
|
||||
}
|
||||
String methodPrefix = method.trim().toUpperCase() + ":";
|
||||
String actionSuffix = ":" + action.name();
|
||||
for (String allowedRequest : ALLOWED_REQUESTS) {
|
||||
if (allowedRequest.startsWith(methodPrefix)
|
||||
&& allowedRequest.endsWith(actionSuffix)
|
||||
&& requestUri.endsWith(requestUriSuffix(allowedRequest))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求授权键。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @param path 请求路径
|
||||
* @param action 资源动作
|
||||
* @return 授权键
|
||||
*/
|
||||
private static String permissionKey(String method, String path, ResourceAction action) {
|
||||
return method + ":" + path + ":" + action.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从授权键中读取请求路径。
|
||||
*
|
||||
* @param permissionKey 授权键
|
||||
* @return 请求路径
|
||||
*/
|
||||
private static String requestUriSuffix(String permissionKey) {
|
||||
int firstSeparator = permissionKey.indexOf(':');
|
||||
int lastSeparator = permissionKey.lastIndexOf(':');
|
||||
return permissionKey.substring(firstSeparator + 1, lastSeparator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package tech.easyflow.ai.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 工作流协作分享创建结果。
|
||||
*/
|
||||
public class WorkflowShareCreateResult implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private BigInteger id;
|
||||
private String shareKey;
|
||||
private String shareUrl;
|
||||
private Date expiresAt;
|
||||
|
||||
/**
|
||||
* 获取分享记录 ID。
|
||||
*
|
||||
* @return 分享记录 ID
|
||||
*/
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分享记录 ID。
|
||||
*
|
||||
* @param id 分享记录 ID
|
||||
*/
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始分享密钥。
|
||||
*
|
||||
* @return 原始分享密钥
|
||||
*/
|
||||
public String getShareKey() {
|
||||
return shareKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置原始分享密钥。
|
||||
*
|
||||
* @param shareKey 原始分享密钥
|
||||
*/
|
||||
public void setShareKey(String shareKey) {
|
||||
this.shareKey = shareKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享 URL。
|
||||
*
|
||||
* @return 分享 URL
|
||||
*/
|
||||
public String getShareUrl() {
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分享 URL。
|
||||
*
|
||||
* @param shareUrl 分享 URL
|
||||
*/
|
||||
public void setShareUrl(String shareUrl) {
|
||||
this.shareUrl = shareUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间。
|
||||
*
|
||||
* @return 过期时间
|
||||
*/
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间。
|
||||
*
|
||||
* @param expiresAt 过期时间
|
||||
*/
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.easyflow.ai.share;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* V34 工作流分享与审批说明迁移契约测试。
|
||||
*/
|
||||
public class WorkflowShareMigrationContractTest {
|
||||
|
||||
/**
|
||||
* 验证迁移同时创建审批说明、工作流修订号和分享记录所需字段及索引。
|
||||
*
|
||||
* @throws Exception 迁移文件不可读时抛出
|
||||
*/
|
||||
@Test
|
||||
public void migrationShouldCreateWorkflowShareContracts() throws Exception {
|
||||
String sql = migrationSql();
|
||||
|
||||
assertTrue(sql.contains("ADD COLUMN `application_reason` VARCHAR(500)"));
|
||||
assertTrue(sql.contains("ADD COLUMN `revision` INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("CREATE TABLE `tb_workflow_share`"));
|
||||
assertTrue(sql.contains("`share_key_hash` VARCHAR(64) NOT NULL"));
|
||||
assertTrue(sql.contains("`status` VARCHAR(32) NOT NULL"));
|
||||
assertTrue(sql.contains("`dept_id` BIGINT UNSIGNED NULL"));
|
||||
assertTrue(sql.contains("`modified` DATETIME NULL"));
|
||||
assertTrue(sql.contains("UNIQUE INDEX `uni_workflow_share_key_hash`"));
|
||||
assertTrue(sql.contains("INDEX `idx_workflow_share_status` (`workflow_id`, `status`)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工作区中的 V34 MySQL 迁移。
|
||||
*
|
||||
* @return 迁移 SQL
|
||||
* @throws Exception 迁移文件不存在或不可读时抛出
|
||||
*/
|
||||
private String migrationSql() throws Exception {
|
||||
Path root = Path.of(System.getProperty("maven.multiModuleProjectDirectory",
|
||||
Path.of(System.getProperty("user.dir")).toAbsolutePath().toString()));
|
||||
while (root != null) {
|
||||
Path migration = root.resolve("easyflow-starter/easyflow-starter-all/src/main/resources/"
|
||||
+ "db/migration/mysql/V34__mysql_workflow_share_and_approval_reason.sql");
|
||||
if (Files.isRegularFile(migration)) {
|
||||
return Files.readString(migration, StandardCharsets.UTF_8);
|
||||
}
|
||||
root = root.getParent();
|
||||
}
|
||||
throw new IllegalStateException("找不到 V34 工作流分享与审批说明迁移");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tech.easyflow.ai.share;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* {@link WorkflowSharePolicy} 测试。
|
||||
*/
|
||||
public class WorkflowSharePolicyTest {
|
||||
|
||||
/**
|
||||
* 验证相同密钥生成稳定的 SHA-256 摘要。
|
||||
*/
|
||||
@Test
|
||||
public void shouldHashShareKeyDeterministically() {
|
||||
String first = WorkflowSharePolicy.hashShareKey("share-key");
|
||||
String second = WorkflowSharePolicy.hashShareKey("share-key");
|
||||
|
||||
Assert.assertEquals(first, second);
|
||||
Assert.assertEquals(64, first.length());
|
||||
Assert.assertNotEquals("share-key", first);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证默认过期时间为创建时间后 30 分钟。
|
||||
*/
|
||||
@Test
|
||||
public void shouldExpireThirtyMinutesAfterCreation() {
|
||||
Date createdAt = new Date(1_000L);
|
||||
|
||||
Date expiresAt = WorkflowSharePolicy.defaultExpiresAt(createdAt);
|
||||
|
||||
Assert.assertEquals(30 * 60 * 1_000L, expiresAt.getTime() - createdAt.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分享授权仅覆盖编辑、运行和发布所需接口。
|
||||
*/
|
||||
@Test
|
||||
public void shouldAllowOnlyCollaborativeWorkflowOperations() {
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"GET",
|
||||
"/api/v1/workflow/detail",
|
||||
ResourceAction.READ
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/update",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/runAsync",
|
||||
ResourceAction.USE
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/submitPublishApproval",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证删除、下线和再次分享不在协作授权范围内。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectSensitiveWorkflowOperations() {
|
||||
Assert.assertFalse(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/submitDeleteApproval",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
Assert.assertFalse(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/submitOfflineApproval",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
Assert.assertFalse(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflowShare/url/create",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
Assert.assertFalse(WorkflowSharePolicy.isAllowedRequest(
|
||||
"GET",
|
||||
"/api/v1/workflow/update",
|
||||
ResourceAction.MANAGE
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user