diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployAppConfigServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployAppConfigServiceImpl.java index 043f9bc7..8e9d58f0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployAppConfigServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployAppConfigServiceImpl.java @@ -154,7 +154,7 @@ public class DeployAppConfigServiceImpl extends BaseServiceImpl() { + request.setFormData(objectMapper.convertValue(dto.getBuildVariables(), new TypeReference<>() { })); WorkflowInstanceDTO workflowInstanceDTO = workflowInstanceService.startWorkflow(request); buildAndSaveDeployLog(dto, environment, application, workflowInstanceDTO); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/CustomUserDetails.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/CustomUserDetails.java new file mode 100644 index 00000000..6c30ec58 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/CustomUserDetails.java @@ -0,0 +1,140 @@ +package com.qqchen.deploy.backend.framework.security; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/** + * 自定义 UserDetails 实现 + * 扩展标准 UserDetails,包含用户ID、租户ID等额外信息 + * + * @author qqchen + * @date 2025-10-25 + */ +@Getter +public class CustomUserDetails implements UserDetails { + + /** + * 用户ID + */ + private final Long userId; + + /** + * 用户名 + */ + private final String username; + + /** + * 密码 + */ + private final String password; + + /** + * 是否启用 + */ + private final boolean enabled; + + /** + * 账户是否未过期 + */ + private final boolean accountNonExpired; + + /** + * 账户是否未锁定 + */ + private final boolean accountNonLocked; + + /** + * 凭证是否未过期 + */ + private final boolean credentialsNonExpired; + + /** + * 权限列表 + */ + private final Collection authorities; + + /** + * 租户ID(可选) + */ + private final Long tenantId; + + /** + * 部门ID(可选) + */ + private final Long departmentId; + + /** + * 构造函数 + */ + public CustomUserDetails(Long userId, + String username, + String password, + boolean enabled, + boolean accountNonExpired, + boolean accountNonLocked, + boolean credentialsNonExpired, + Collection authorities, + Long tenantId, + Long departmentId) { + this.userId = userId; + this.username = username; + this.password = password; + this.enabled = enabled; + this.accountNonExpired = accountNonExpired; + this.accountNonLocked = accountNonLocked; + this.credentialsNonExpired = credentialsNonExpired; + this.authorities = authorities; + this.tenantId = tenantId; + this.departmentId = departmentId; + } + + /** + * 简化构造函数(不包含租户和部门信息) + */ + public CustomUserDetails(Long userId, + String username, + String password, + boolean enabled, + Collection authorities) { + this(userId, username, password, enabled, true, true, true, authorities, null, null); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/SecurityUtils.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/SecurityUtils.java index ab157039..aba9ac00 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/SecurityUtils.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/SecurityUtils.java @@ -4,10 +4,19 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +/** + * Security 工具类 + * 提供获取当前用户信息、权限检查等功能 + * + * @author qqchen + * @date 2025-10-25 + */ public class SecurityUtils { /** * 获取当前登录用户名 + * + * @return 当前用户名,未认证时返回 "SYSTEM" */ public static String getCurrentUsername() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -22,8 +31,79 @@ public class SecurityUtils { return principal.toString(); } + /** + * 获取当前用户ID(字符串类型) + * + * @return 当前用户ID,未认证时返回 "system" + */ + public static String getCurrentUserIdAsString() { + Long userId = getCurrentUserId(); + return userId != null ? userId.toString() : "system"; + } + + /** + * 获取当前用户ID(Long 类型) + * + * @return 当前用户ID,未认证时返回 null + */ + public static Long getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof CustomUserDetails) { + return ((CustomUserDetails) principal).getUserId(); + } + + // 兼容标准 UserDetails(用于测试或特殊场景) + return null; + } + + /** + * 获取当前租户ID + * + * @return 当前租户ID,未认证或未设置时返回 null + */ + public static Long getCurrentTenantId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof CustomUserDetails) { + return ((CustomUserDetails) principal).getTenantId(); + } + + return null; + } + + /** + * 获取当前部门ID + * + * @return 当前部门ID,未认证或未设置时返回 null + */ + public static Long getCurrentDepartmentId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof CustomUserDetails) { + return ((CustomUserDetails) principal).getDepartmentId(); + } + + return null; + } + /** * 检查是否有指定权限 + * + * @param permission 权限标识 + * @return 是否拥有该权限 */ public static boolean hasPermission(String permission) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -33,22 +113,10 @@ public class SecurityUtils { .anyMatch(auth -> auth.getAuthority().equals(permission)); } - /** - * 获取当前用户ID - */ - public static Long getCurrentUserId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails) { - return Long.valueOf(((UserDetails) principal).getUsername()); - } - } - return null; - } - /** * 检查是否已认证 + * + * @return 是否已认证 */ public static boolean isAuthenticated() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java index 0523351c..aae1b6e8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java @@ -1,5 +1,6 @@ package com.qqchen.deploy.backend.system.service.impl; +import com.qqchen.deploy.backend.framework.security.CustomUserDetails; import com.qqchen.deploy.backend.system.entity.User; import com.qqchen.deploy.backend.system.repository.IUserRepository; import jakarta.annotation.Resource; @@ -12,6 +13,13 @@ import org.springframework.stereotype.Service; import java.util.Collections; +/** + * 用户详情服务实现 + * 负责从数据库加载用户信息并转换为 Spring Security 的 UserDetails + * + * @author qqchen + * @date 2025-10-25 + */ @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { @@ -22,15 +30,15 @@ public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsernameAndDeletedFalse(username) - .orElseThrow(() -> new UsernameNotFoundException("用户不存在")); - return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), - user.getEnabled(), - true, - true, - true, - Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); + + // ✅ 返回自定义 UserDetails,包含用户ID等扩展信息 + return new CustomUserDetails( + user.getId(), // 用户ID + user.getUsername(), // 用户名 + user.getPassword(), // 密码 + user.getEnabled(), // 是否启用 + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) // 权限(TODO: 从数据库加载实际权限) ); } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java index 8e341bfb..1333c491 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java @@ -1,9 +1,19 @@ package com.qqchen.deploy.backend.workflow.converter; import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.framework.security.SecurityUtils; +import com.qqchen.deploy.backend.workflow.dto.FormDataCreateFromWorkflowRequest; import com.qqchen.deploy.backend.workflow.dto.FormDataDTO; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; import com.qqchen.deploy.backend.workflow.entity.FormData; +import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +import java.time.LocalDateTime; /** * 表单数据转换器 @@ -13,5 +23,48 @@ import org.mapstruct.Mapper; */ @Mapper(config = BaseConverter.class) public interface FormDataConverter extends BaseConverter { + + /** + * 从工作流启动创建表单数据 DTO + * 从 FormDefinitionDTO 和 FormDataCreateFromWorkflowRequest 映射到 FormDataDTO + * + * @param formDefinition 表单定义 + * @param request 工作流表单数据请求 + * @return 表单数据 DTO + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "formDefinitionId", source = "formDefinition.id") + @Mapping(target = "formKey", source = "formDefinition.key") + @Mapping(target = "formVersion", source = "formDefinition.formVersion") + @Mapping(target = "categoryId", source = "formDefinition.categoryId") + @Mapping(target = "schemaSnapshot", source = "formDefinition.schema") + @Mapping(target = "data", source = "request.formData") + @Mapping(target = "businessKey", source = "request.businessKey") + @Mapping(target = "businessType", constant = "WORKFLOW") + @Mapping(target = "status", constant = "SUBMITTED") + @Mapping(target = "submitter", ignore = true) // 在 @AfterMapping 中设置 + @Mapping(target = "submitTime", ignore = true) // 在 @AfterMapping 中设置 + @Mapping(target = "createTime", ignore = true) + @Mapping(target = "updateTime", ignore = true) + @Mapping(target = "createBy", ignore = true) + @Mapping(target = "updateBy", ignore = true) + @Mapping(target = "version", ignore = true) + @Mapping(target = "deleted", ignore = true) + FormDataDTO fromWorkflowRequest( + FormDefinitionDTO formDefinition, + FormDataCreateFromWorkflowRequest request + ); + + /** + * 后处理:设置动态值 + */ + @AfterMapping + default void setDynamicFields(@MappingTarget FormDataDTO formDataDTO) { + // ✅ 设置当前用户 + formDataDTO.setSubmitter(SecurityUtils.getCurrentUserIdAsString()); + + // ✅ 设置提交时间 + formDataDTO.setSubmitTime(LocalDateTime.now()); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java index 219c378d..468e02cd 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java @@ -4,22 +4,76 @@ import com.qqchen.deploy.backend.framework.converter.BaseConverter; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowHistoricalInstancesDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; +import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceStartRequest; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; -import org.mapstruct.Mapper; -import org.mapstruct.Named; +import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums; +import org.flowable.engine.runtime.ProcessInstance; +import org.mapstruct.*; + +import java.time.LocalDateTime; /** - * 工作流定义转换器 + * 工作流实例转换器 */ @Mapper(config = BaseConverter.class) public interface WorkflowInstanceConverter extends BaseConverter { - @Override @Named("toDto") WorkflowInstanceDTO toDto(WorkflowInstance entity); @Named("toHistoricalDto") WorkflowHistoricalInstancesDTO toWorkflowHistoricalInstancesDTO(WorkflowInstance workflowInstance); + + /** + * 从多个源对象创建 WorkflowInstance + * + * @param request 工作流启动请求 + * @param processInstance Flowable 流程实例 + * @param definition 工作流定义 + * @param formDataId 表单数据ID(可选) + * @return 工作流实例 + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "processInstanceId", source = "processInstance.id") + @Mapping(target = "processDefinitionId", source = "processInstance.processDefinitionId") + @Mapping(target = "workflowDefinitionId", source = "definition.id") + @Mapping(target = "businessKey", source = "request.businessKey") + @Mapping(target = "graphSnapshot", source = "definition.graph") + @Mapping(target = "formDataId", source = "formDataId") + @Mapping(target = "formData", ignore = true) // 在 @AfterMapping 中处理 + @Mapping(target = "status", constant = "NOT_STARTED") + @Mapping(target = "startTime", ignore = true) // 在 @AfterMapping 中设置 + @Mapping(target = "endTime", ignore = true) + @Mapping(target = "createTime", ignore = true) + @Mapping(target = "updateTime", ignore = true) + @Mapping(target = "createBy", ignore = true) + @Mapping(target = "updateBy", ignore = true) + @Mapping(target = "version", ignore = true) + @Mapping(target = "deleted", ignore = true) + WorkflowInstance toEntity( + WorkflowInstanceStartRequest request, + ProcessInstance processInstance, + WorkflowDefinition definition, + Long formDataId + ); + + /** + * 后处理:设置 formData 和 startTime + */ + @AfterMapping + default void afterMapping( + WorkflowInstanceStartRequest request, + Long formDataId, + @MappingTarget WorkflowInstance workflowInstance + ) { + // ✅ 只有在没有 FormData ID 时才保存 JSON + if (formDataId == null && request.getFormData() != null) { + workflowInstance.setFormData(request.getFormData()); + } + + // ✅ 设置开始时间 + workflowInstance.setStartTime(LocalDateTime.now()); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataCreateFromWorkflowRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataCreateFromWorkflowRequest.java new file mode 100644 index 00000000..4d194ad8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataCreateFromWorkflowRequest.java @@ -0,0 +1,27 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +/** + * 从工作流启动创建表单数据请求 + * + * @author qqchen + * @date 2025-10-25 + */ +@Data +@Schema(description = "从工作流启动创建表单数据请求") +public class FormDataCreateFromWorkflowRequest { + + @Schema(description = "表单标识", required = true, example = "APPROVAL-AND-DEPLOYMENT") + private String formKey; + + @Schema(description = "表单填写数据", required = true) + private Map formData; + + @Schema(description = "业务标识(工作流实例的 businessKey)", required = true, example = "DEPLOY-2023-001") + private String businessKey; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceDTO.java index b57ce2a2..de598d9c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceDTO.java @@ -34,20 +34,16 @@ public class WorkflowInstanceDTO extends BaseDTO { */ private Long formDataId; - /** - * 启动表单数据(用于展示) - */ - private FormDataDTO formData; - /** * 实例状态 */ private WorkflowInstanceStatusEnums status; /** - * 流程变量(JSON) + * 表单数据(JSON) + * 存储流程启动时传入的表单变量 */ - private String variables; + private Map formData; /** * 开始时间 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceStartRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceStartRequest.java index 9e3b939d..51a3317b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceStartRequest.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowInstanceStartRequest.java @@ -9,10 +9,16 @@ import java.util.Map; @Schema(description = "工作流实例启动入参") public class WorkflowInstanceStartRequest { + @Schema(description = "流程定义标识", example = "APPROVAL-AND-DEPLOYMENT") private String processKey; + @Schema(description = "业务键", example = "DEPLOY-2023-001") private String businessKey; - private Map variables; + @Schema(description = "表单标识(可选,如果有则创建 FormData 记录)", example = "APPROVAL-AND-DEPLOYMENT") + private String formKey; + + @Schema(description = "表单数据") + private Map formData; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java index 7094e456..ae373f90 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java @@ -4,6 +4,7 @@ import com.qqchen.deploy.backend.framework.domain.Entity; import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph; import com.qqchen.deploy.backend.workflow.entity.converter.WorkflowGraphType; import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums; +import com.vladmihalcea.hibernate.type.json.JsonType; import jakarta.persistence.Column; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -13,6 +14,7 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import java.time.LocalDateTime; +import java.util.Map; /** * 工作流实例实体 @@ -58,10 +60,12 @@ public class WorkflowInstance extends Entity { private WorkflowInstanceStatusEnums status; /** - * 流程变量(JSON) + * 表单数据(JSON) + * 存储流程启动时传入的表单变量 */ - @Column(columnDefinition = "TEXT") - private String variables; + @Type(JsonType.class) + @Column(name = "form_data", columnDefinition = "json") + private Map formData; /** * 流程图数据快照(启动时保存,用于画布还原) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java index 59a8a0a5..a4081b4b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java @@ -1,6 +1,7 @@ package com.qqchen.deploy.backend.workflow.service; import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.workflow.dto.FormDataCreateFromWorkflowRequest; import com.qqchen.deploy.backend.workflow.dto.FormDataDTO; import com.qqchen.deploy.backend.workflow.dto.query.FormDataQuery; import com.qqchen.deploy.backend.workflow.entity.FormData; @@ -28,5 +29,14 @@ public interface IFormDataService extends IBaseService new RuntimeException("Workflow definition not found: " + workflowDefinitionId)); + WorkflowDefinition definition = workflowDefinitionRepository.findByKey(request.getProcessKey()) + .orElseThrow(() -> new RuntimeException("Workflow definition not found: " + request.getProcessKey())); - // 2. 创建实例并保存 graph 快照 - WorkflowInstance workflowInstance = new WorkflowInstance(); - workflowInstance.setProcessInstanceId(processInstance.getId()); - workflowInstance.setProcessDefinitionId(processInstance.getProcessDefinitionId()); - workflowInstance.setWorkflowDefinitionId(workflowDefinitionId); - workflowInstance.setBusinessKey(businessKey); - workflowInstance.setGraphSnapshot(definition.getGraph()); // ✅ 保存画布数据快照 - workflowInstance.setStatus(WorkflowInstanceStatusEnums.NOT_STARTED); - workflowInstance.setStartTime(LocalDateTime.now()); + // 2. 使用 MapStruct Converter 创建实例(处理多源对象映射) + WorkflowInstance workflowInstance = workflowInstanceConverter.toEntity( + request, + processInstance, + definition, + formDataId + ); workflowInstanceRepository.save(workflowInstance); - log.info("创建工作流实例: instanceId={}, definitionId={}, graph已保存", - workflowInstance.getId(), workflowDefinitionId); + log.info("创建工作流实例: instanceId={}, definitionId={}, formDataId={}, graph已保存", + workflowInstance.getId(), definition.getId(), formDataId); // 3. 返回创建结果 return workflowInstanceConverter.toDto(workflowInstance); @@ -170,11 +178,8 @@ public class WorkflowInstanceServiceImpl extends BaseServiceImpl new RuntimeException("Workflow instance not found: " + processInstanceId)); - try { - instance.setVariables(objectMapper.writeValueAsString(variables)); - } catch (JsonProcessingException e) { - log.error("Failed to serialize variables", e); - } + // ✅ 不再更新 formData,因为它应该保持启动时的数据不变 + // formData 字段只在流程启动时设置一次,用于记录启动表单数据 instance.setStatus(WorkflowInstanceStatusEnums.COMPLETED); instance.setEndTime(LocalDateTime.now()); @@ -185,28 +190,59 @@ public class WorkflowInstanceServiceImpl extends BaseServiceImpl new RuntimeException("Workflow definition process key not found: " + request.getProcessKey())); - - // ✅ 将前端传入的 variables 包装到 "form" 变量中 - // 这样流程中的 ${form.xxx} 表达式就能正确解析 - Map variables = new HashMap<>(); - if (request.getVariables() != null && !request.getVariables().isEmpty()) { - variables.put("form", request.getVariables()); + // ✅ 步骤1: 先创建 FormData(如果需要) + FormDataDTO formData = null; + if (request.getFormKey() != null && request.getFormData() != null && !request.getFormData().isEmpty()) { + FormDataCreateFromWorkflowRequest formDataRequest = new FormDataCreateFromWorkflowRequest(); + formDataRequest.setFormKey(request.getFormKey()); + formDataRequest.setFormData(request.getFormData()); + formDataRequest.setBusinessKey(request.getBusinessKey()); + + formData = formDataService.createFromWorkflowStart(formDataRequest); + log.info("FormData 已创建: id={}", formData.getId()); + } else { + log.info("FormData 创建: 跳过(无 formKey 或表单数据)"); } + // ✅ 步骤2: 构建 Flowable 变量 + Map variables = buildFlowableVariables(request); + + // ✅ 步骤3: 启动 Flowable 流程 ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder() .processDefinitionKey(request.getProcessKey()) - .variables(variables) // 使用包装后的变量 + .variables(variables) .businessKey(request.getBusinessKey()) .startAsync(); // 异步启动,会自动执行 shell 任务 -// .start(); - return createWorkflowInstance(workflowDefinition.getId(), request.getBusinessKey(), processInstance); + + log.info("Flowable 流程已启动: processInstanceId={}", processInstance.getId()); + + // ✅ 步骤4: 创建 WorkflowInstance 记录(关联 FormData) + return createWorkflowInstance(request, processInstance, formData != null ? formData.getId() : null); } catch (Exception e) { - log.error("Failed to create workflow: {}", request.getProcessKey(), e); - throw new RuntimeException("Failed to create workflow", e); + log.error("启动工作流失败: processKey={}", request.getProcessKey(), e); + throw new RuntimeException("启动工作流失败: " + e.getMessage(), e); } } + /** + * 构建 Flowable 流程变量 + * + * @param request 工作流启动请求 + * @return Flowable 变量 Map + */ + private Map buildFlowableVariables(WorkflowInstanceStartRequest request) { + Map variables = new HashMap<>(); + + // 将前端传入的 formData 包装到 "form" 变量中 + // 这样流程中的 ${form.xxx} 表达式就能正确解析 + Map formData = request.getFormData() != null ? request.getFormData() : new HashMap<>(); + if (!formData.isEmpty()) { + variables.put("form", formData); + } + + return variables; + } + @Override public Page findTemplatesWithRecentInstances(WorkflowDefinitionQuery query) { Pageable pageable = PageRequest.of(query.getPageNum(), query.getPageSize()); diff --git a/backend/src/main/resources/db/changelog/archive/V1.0.0__init_schema.sql b/backend/src/main/resources/db/changelog/archive/V1.0.0__init_schema.sql index 99286eef..3ec68dc9 100644 --- a/backend/src/main/resources/db/changelog/archive/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/changelog/archive/V1.0.0__init_schema.sql @@ -452,8 +452,10 @@ CREATE TABLE workflow_instance process_definition_id VARCHAR(64) NOT NULL COMMENT '流程定义ID', workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', business_key VARCHAR(64) NULL COMMENT '业务标识', + form_data_id BIGINT NULL COMMENT '启动表单数据ID(外键关联form_data)', status VARCHAR(100) NOT NULL COMMENT '实例状态', - variables TEXT NULL COMMENT '流程变量(JSON)', + form_data JSON NULL COMMENT '表单数据(JSON)', + graph_snapshot JSON NULL COMMENT '流程图数据快照(启动时保存,用于画布还原)', start_time DATETIME(6) NULL COMMENT '开始时间', end_time DATETIME(6) NULL COMMENT '结束时间'