From 96296270bbbd74b0da49ce9d047e4dbb4ce9296b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 24 Oct 2025 10:31:40 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0form=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/framework/enums/ResponseCode.java | 6 +- .../workflow/api/FormDataApiController.java | 52 +++++++ .../api/FormDefinitionApiController.java | 54 ++++++++ .../workflow/converter/FormDataConverter.java | 17 +++ .../converter/FormDefinitionConverter.java | 17 +++ .../backend/workflow/dto/FormDataDTO.java | 85 ++++++++++++ .../workflow/dto/FormDefinitionDTO.java | 78 +++++++++++ .../backend/workflow/dto/form/FormSchema.java | 79 +++++++++++ .../workflow/dto/query/FormDataQuery.java | 57 ++++++++ .../dto/query/FormDefinitionQuery.java | 57 ++++++++ .../backend/workflow/entity/FormData.java | 95 +++++++++++++ .../workflow/entity/FormDefinition.java | 88 ++++++++++++ .../entity/converter/FormSchemaType.java | 131 ++++++++++++++++++ .../workflow/enums/FormBusinessTypeEnums.java | 28 ++++ .../workflow/enums/FormCategoryEnums.java | 29 ++++ .../workflow/enums/FormDataStatusEnums.java | 29 ++++ .../enums/FormDefinitionStatusEnums.java | 28 ++++ .../repository/IFormDataRepository.java | 65 +++++++++ .../repository/IFormDefinitionRepository.java | 64 +++++++++ .../workflow/service/IFormDataService.java | 32 +++++ .../service/IFormDefinitionService.java | 33 +++++ .../service/impl/FormDataServiceImpl.java | 74 ++++++++++ .../impl/FormDefinitionServiceImpl.java | 88 ++++++++++++ .../db/changelog/changes/v1.0.0-schema.sql | 88 ++++++++++++ 24 files changed, 1373 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDefinitionConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/form/FormSchema.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDataQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormData.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/converter/FormSchemaType.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormBusinessTypeEnums.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDataStatusEnums.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDefinitionStatusEnums.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDataRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDefinitionRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDefinitionService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDataServiceImpl.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index 19d12afd..bbdd0b38 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -149,7 +149,11 @@ public enum ResponseCode { /** * 工作流变量反序列化错误 */ - WORKFLOW_VARIABLE_DESERIALIZE_ERROR(50302, "workflow.variable.deserialize.error"); + WORKFLOW_VARIABLE_DESERIALIZE_ERROR(50302, "workflow.variable.deserialize.error"), + + // 表单管理相关错误码 (2800-2899) + FORM_DEFINITION_NOT_FOUND(2800, "form.definition.not.found"), + FORM_DATA_NOT_FOUND(2801, "form.data.not.found"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java new file mode 100644 index 00000000..7adcabb8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java @@ -0,0 +1,52 @@ +package com.qqchen.deploy.backend.workflow.api; + +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +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; +import com.qqchen.deploy.backend.workflow.service.IFormDataService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +/** + * 表单数据API控制器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/workflow/form-data") +@Tag(name = "表单数据管理", description = "表单数据管理相关接口") +public class FormDataApiController extends BaseController { + + @Resource + private IFormDataService formDataService; + + @Operation(summary = "提交表单数据") + @PostMapping("/submit") + public Response submit(@RequestBody FormDataDTO dto) { + FormDataDTO result = formDataService.submit(dto); + return Response.success(result); + } + + @Operation(summary = "根据业务标识查询表单数据") + @GetMapping("/business/{businessKey}") + public Response getByBusinessKey( + @Parameter(description = "业务标识", required = true) @PathVariable String businessKey + ) { + FormDataDTO result = formDataService.getByBusinessKey(businessKey); + return Response.success(result); + } + + @Override + protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List data) { + // 暂不支持导出 + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java new file mode 100644 index 00000000..81f44ee5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java @@ -0,0 +1,54 @@ +package com.qqchen.deploy.backend.workflow.api; + +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import com.qqchen.deploy.backend.workflow.service.IFormDefinitionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +/** + * 表单定义API控制器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/workflow/form-definition") +@Tag(name = "表单定义管理", description = "表单定义管理相关接口") +public class FormDefinitionApiController extends BaseController { + + @Resource + private IFormDefinitionService formDefinitionService; + + @Operation(summary = "发布表单") + @PostMapping("/{id}/publish") + public Response publish( + @Parameter(description = "表单定义ID", required = true) @PathVariable Long id + ) { + FormDefinitionDTO result = formDefinitionService.publish(id); + return Response.success(result); + } + + @Operation(summary = "根据key获取最新版本表单") + @GetMapping("/latest/{key}") + public Response getLatestByKey( + @Parameter(description = "表单标识", required = true) @PathVariable String key + ) { + FormDefinitionDTO result = formDefinitionService.getLatestByKey(key); + return Response.success(result); + } + + @Override + protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List data) { + // 暂不支持导出 + } +} + 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 new file mode 100644 index 00000000..8e341bfb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDataConverter.java @@ -0,0 +1,17 @@ +package com.qqchen.deploy.backend.workflow.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.workflow.dto.FormDataDTO; +import com.qqchen.deploy.backend.workflow.entity.FormData; +import org.mapstruct.Mapper; + +/** + * 表单数据转换器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Mapper(config = BaseConverter.class) +public interface FormDataConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDefinitionConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDefinitionConverter.java new file mode 100644 index 00000000..e991f0d7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormDefinitionConverter.java @@ -0,0 +1,17 @@ +package com.qqchen.deploy.backend.workflow.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import org.mapstruct.Mapper; + +/** + * 表单定义转换器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Mapper(config = BaseConverter.class) +public interface FormDefinitionConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataDTO.java new file mode 100644 index 00000000..c5d68ad8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDataDTO.java @@ -0,0 +1,85 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; +import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 表单数据DTO + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "表单数据DTO") +public class FormDataDTO extends BaseDTO { + + /** + * 表单定义ID + */ + @Schema(description = "表单定义ID", example = "1") + private Long formDefinitionId; + + /** + * 表单标识(冗余存储,避免JOIN) + */ + @Schema(description = "表单标识", example = "employee_info_form") + private String formKey; + + /** + * 表单版本(冗余存储,用于历史追溯) + */ + @Schema(description = "表单版本", example = "1") + private Integer formVersion; + + /** + * 业务标识(如工作流实例ID、订单号等) + */ + @Schema(description = "业务标识", example = "workflow_instance_123") + private String businessKey; + + /** + * 业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单) + */ + @Schema(description = "业务类型") + private FormBusinessTypeEnums businessType; + + /** + * 表单填写数据(用户提交的实际数据) + */ + @Schema(description = "表单填写数据") + private Map data; + + /** + * 表单Schema快照(用于历史追溯,确保数据可还原) + */ + @Schema(description = "表单Schema快照") + private FormSchema schemaSnapshot; + + /** + * 提交人 + */ + @Schema(description = "提交人", example = "admin") + private String submitter; + + /** + * 提交时间 + */ + @Schema(description = "提交时间", example = "2025-10-24T12:00:00") + private LocalDateTime submitTime; + + /** + * 状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝) + */ + @Schema(description = "状态") + private FormDataStatusEnums status; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java new file mode 100644 index 00000000..47948755 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java @@ -0,0 +1,78 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; +import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 表单定义DTO + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "表单定义DTO") +public class FormDefinitionDTO extends BaseDTO { + + /** + * 表单名称 + */ + @Schema(description = "表单名称", example = "员工信息登记表") + private String name; + + /** + * 表单标识(业务唯一) + */ + @Schema(description = "表单标识", example = "employee_info_form") + private String key; + + /** + * 表单版本号 + */ + @Schema(description = "表单版本号", example = "1") + private Integer formVersion; + + /** + * 表单分类 + */ + @Schema(description = "表单分类") + private FormCategoryEnums category; + + /** + * 表单描述 + */ + @Schema(description = "表单描述", example = "用于采集员工基本信息") + private String description; + + /** + * 表单Schema(前端设计器导出的JSON结构) + */ + @Schema(description = "表单Schema") + private FormSchema schema; + + /** + * 标签(用于分类和搜索) + */ + @Schema(description = "标签列表", example = "[\"员工管理\", \"信息采集\"]") + private List tags; + + /** + * 状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用) + */ + @Schema(description = "状态") + private FormDefinitionStatusEnums status; + + /** + * 是否为模板 + */ + @Schema(description = "是否为模板", example = "false") + private Boolean isTemplate; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/form/FormSchema.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/form/FormSchema.java new file mode 100644 index 00000000..acbcb5dc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/form/FormSchema.java @@ -0,0 +1,79 @@ +package com.qqchen.deploy.backend.workflow.dto.form; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 表单Schema定义 + * 用于存储前端表单设计器导出的JSON结构 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +public class FormSchema { + + /** + * 表单标题 + */ + private String title; + + /** + * 表单描述 + */ + private String description; + + /** + * 表单字段列表 + */ + private List fields; + + /** + * 表单配置 + */ + private Map config; + + /** + * 表单字段定义 + */ + @Data + public static class FormField { + /** + * 字段名称 + */ + private String name; + + /** + * 字段标签 + */ + private String label; + + /** + * 字段类型(input、select、textarea、date、number等) + */ + private String type; + + /** + * 是否必填 + */ + private Boolean required; + + /** + * 默认值 + */ + private Object defaultValue; + + /** + * 字段配置(不同类型字段有不同配置) + */ + private Map config; + + /** + * 验证规则 + */ + private List> validators; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDataQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDataQuery.java new file mode 100644 index 00000000..5df5abfe --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDataQuery.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.workflow.dto.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 表单数据查询对象 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FormDataQuery extends BaseQuery { + + /** + * 表单定义ID(精确查询) + */ + @QueryField(field = "formDefinitionId", type = QueryType.EQUAL) + private Long formDefinitionId; + + /** + * 表单标识(精确查询) + */ + @QueryField(field = "formKey", type = QueryType.EQUAL) + private String formKey; + + /** + * 业务标识(精确查询) + */ + @QueryField(field = "businessKey", type = QueryType.EQUAL) + private String businessKey; + + /** + * 业务类型(精确查询) + */ + @QueryField(field = "businessType", type = QueryType.EQUAL) + private FormBusinessTypeEnums businessType; + + /** + * 提交人(精确查询) + */ + @QueryField(field = "submitter", type = QueryType.EQUAL) + private String submitter; + + /** + * 状态(精确查询) + */ + @QueryField(field = "status", type = QueryType.EQUAL) + private FormDataStatusEnums status; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java new file mode 100644 index 00000000..7e0a808d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.workflow.dto.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 表单定义查询对象 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FormDefinitionQuery extends BaseQuery { + + /** + * 表单名称(模糊查询) + */ + @QueryField(field = "name", type = QueryType.LIKE) + private String name; + + /** + * 表单标识(精确查询) + */ + @QueryField(field = "key", type = QueryType.EQUAL) + private String key; + + /** + * 表单版本(精确查询) + */ + @QueryField(field = "formVersion", type = QueryType.EQUAL) + private Integer formVersion; + + /** + * 表单分类(精确查询) + */ + @QueryField(field = "category", type = QueryType.EQUAL) + private FormCategoryEnums category; + + /** + * 状态(精确查询) + */ + @QueryField(field = "status", type = QueryType.EQUAL) + private FormDefinitionStatusEnums status; + + /** + * 是否为模板(精确查询) + */ + @QueryField(field = "isTemplate", type = QueryType.EQUAL) + private Boolean isTemplate; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormData.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormData.java new file mode 100644 index 00000000..92b7da68 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormData.java @@ -0,0 +1,95 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; +import com.qqchen.deploy.backend.workflow.entity.converter.FormSchemaType; +import com.qqchen.deploy.backend.workflow.enums.FormBusinessTypeEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import com.vladmihalcea.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 表单数据实体 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@Table(name = "form_data") +@jakarta.persistence.Entity +@EqualsAndHashCode(callSuper = true) +@LogicDelete +public class FormData extends Entity { + + /** + * 表单定义ID + */ + @Column(name = "form_definition_id", nullable = false) + private Long formDefinitionId; + + /** + * 表单标识(冗余存储,避免JOIN) + */ + @Column(name = "form_key", nullable = false) + private String formKey; + + /** + * 表单版本(冗余存储,用于历史追溯) + */ + @Column(name = "form_version", nullable = false) + private Integer formVersion; + + /** + * 业务标识(如工作流实例ID、订单号等) + */ + @Column(name = "business_key") + private String businessKey; + + /** + * 业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单) + */ + @Column(name = "business_type") + @Enumerated(EnumType.STRING) + private FormBusinessTypeEnums businessType; + + /** + * 表单填写数据(用户提交的实际数据) + */ + @Type(JsonType.class) + @Column(name = "data", nullable = false, columnDefinition = "json") + private Map data; + + /** + * 表单Schema快照(用于历史追溯,确保数据可还原) + */ + @Type(FormSchemaType.class) + @Column(name = "schema_snapshot", nullable = false, columnDefinition = "json") + private FormSchema schemaSnapshot; + + /** + * 提交人 + */ + @Column(name = "submitter") + private String submitter; + + /** + * 提交时间 + */ + @Column(name = "submit_time") + private LocalDateTime submitTime; + + /** + * 状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝) + */ + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private FormDataStatusEnums status; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java new file mode 100644 index 00000000..7ce04baa --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java @@ -0,0 +1,88 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; +import com.qqchen.deploy.backend.workflow.entity.converter.FormSchemaType; +import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; +import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; +import com.vladmihalcea.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; + +import java.util.List; + +/** + * 表单定义实体 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@Table(name = "form_definition") +@jakarta.persistence.Entity +@EqualsAndHashCode(callSuper = true) +@LogicDelete +public class FormDefinition extends Entity { + + /** + * 表单名称 + */ + @Column(nullable = false) + private String name; + + /** + * 表单标识(业务唯一) + */ + @Column(name = "`key`", nullable = false) + private String key; + + /** + * 表单版本号 + */ + @Column(name = "form_version", nullable = false) + private Integer formVersion; + + /** + * 表单分类 + */ + @Column(name = "category") + @Enumerated(EnumType.STRING) + private FormCategoryEnums category; + + /** + * 表单描述 + */ + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + /** + * 表单Schema(前端设计器导出的JSON结构) + */ + @Type(FormSchemaType.class) + @Column(name = "schema", nullable = false, columnDefinition = "json") + private FormSchema schema; + + /** + * 标签(用于分类和搜索) + */ + @Type(JsonType.class) + @Column(name = "tags", columnDefinition = "json") + private List tags; + + /** + * 状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用) + */ + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private FormDefinitionStatusEnums status; + + /** + * 是否为模板 + */ + @Column(name = "is_template", nullable = false) + private Boolean isTemplate; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/converter/FormSchemaType.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/converter/FormSchemaType.java new file mode 100644 index 00000000..0e000d88 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/converter/FormSchemaType.java @@ -0,0 +1,131 @@ +package com.qqchen.deploy.backend.workflow.entity.converter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.UserType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * 自定义 Hibernate 类型,用于处理 FormSchema 的序列化和反序列化 + * + * @author qqchen + * @date 2025-10-24 + */ +public class FormSchemaType implements UserType { + private static final Logger log = LoggerFactory.getLogger(FormSchemaType.class); + private final ObjectMapper objectMapper; + + public FormSchemaType() { + this.objectMapper = new ObjectMapper(); + // 配置 ObjectMapper,忽略未知属性 + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Override + public int getSqlType() { + return Types.VARCHAR; + } + + @Override + public Class returnedClass() { + return FormSchema.class; + } + + @Override + public boolean equals(FormSchema x, FormSchema y) { + if (x == y) { + return true; + } + if (x == null || y == null) { + return false; + } + return x.equals(y); + } + + @Override + public int hashCode(FormSchema x) { + return x == null ? 0 : x.hashCode(); + } + + @Override + public FormSchema nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) + throws SQLException { + String value = rs.getString(position); + if (value == null) { + return null; + } + try { + return objectMapper.readValue(value, FormSchema.class); + } catch (JsonProcessingException e) { + // 记录错误日志,但不抛出异常,返回 null 使得单条记录的错误不影响整体查询 + log.error("Failed to convert String to FormSchema: " + value, e); + return null; + } + } + + @Override + public void nullSafeSet(PreparedStatement st, FormSchema value, int index, SharedSessionContractImplementor session) + throws SQLException { + if (value == null) { + st.setNull(index, Types.VARCHAR); + return; + } + try { + st.setString(index, objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException e) { + throw new HibernateException("Failed to convert FormSchema to String: " + value, e); + } + } + + @Override + public FormSchema deepCopy(FormSchema value) { + if (value == null) { + return null; + } + try { + return objectMapper.readValue(objectMapper.writeValueAsString(value), FormSchema.class); + } catch (JsonProcessingException e) { + throw new HibernateException("Failed to deep copy FormSchema", e); + } + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(FormSchema value) { + try { + return value == null ? null : objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new HibernateException("Failed to disassemble FormSchema", e); + } + } + + @Override + public FormSchema assemble(Serializable cached, Object owner) { + try { + return cached == null ? null : objectMapper.readValue(cached.toString(), FormSchema.class); + } catch (JsonProcessingException e) { + throw new HibernateException("Failed to assemble FormSchema", e); + } + } + + @Override + public FormSchema replace(FormSchema detached, FormSchema managed, Object owner) { + return deepCopy(detached); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormBusinessTypeEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormBusinessTypeEnums.java new file mode 100644 index 00000000..aa8460e6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormBusinessTypeEnums.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.enums; + +import lombok.Getter; + +/** + * 表单业务类型枚举 + * + * @author qqchen + * @date 2025-10-24 + */ +@Getter +public enum FormBusinessTypeEnums { + + WORKFLOW("WORKFLOW", "工作流"), + ORDER("ORDER", "订单"), + STANDALONE("STANDALONE", "独立表单"); + + private final String code; + + private final String description; + + FormBusinessTypeEnums(String code, String description) { + this.code = code; + this.description = description; + } + +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java new file mode 100644 index 00000000..1817121e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java @@ -0,0 +1,29 @@ +package com.qqchen.deploy.backend.workflow.enums; + +import lombok.Getter; + +/** + * 表单分类枚举 + * + * @author qqchen + * @date 2025-10-24 + */ +@Getter +public enum FormCategoryEnums { + + APPROVAL("APPROVAL", "审批"), + DATA_COLLECTION("DATA_COLLECTION", "数据采集"), + SURVEY("SURVEY", "问卷调查"), + OTHER("OTHER", "其他"); + + private final String code; + + private final String description; + + FormCategoryEnums(String code, String description) { + this.code = code; + this.description = description; + } + +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDataStatusEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDataStatusEnums.java new file mode 100644 index 00000000..ae4481ee --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDataStatusEnums.java @@ -0,0 +1,29 @@ +package com.qqchen.deploy.backend.workflow.enums; + +import lombok.Getter; + +/** + * 表单数据状态枚举 + * + * @author qqchen + * @date 2025-10-24 + */ +@Getter +public enum FormDataStatusEnums { + + DRAFT("DRAFT", "草稿"), + SUBMITTED("SUBMITTED", "已提交"), + COMPLETED("COMPLETED", "已完成"), + REJECTED("REJECTED", "已拒绝"); + + private final String code; + + private final String description; + + FormDataStatusEnums(String code, String description) { + this.code = code; + this.description = description; + } + +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDefinitionStatusEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDefinitionStatusEnums.java new file mode 100644 index 00000000..1777a733 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormDefinitionStatusEnums.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.enums; + +import lombok.Getter; + +/** + * 表单定义状态枚举 + * + * @author qqchen + * @date 2025-10-24 + */ +@Getter +public enum FormDefinitionStatusEnums { + + DRAFT("DRAFT", "草稿"), + PUBLISHED("PUBLISHED", "已发布"), + DISABLED("DISABLED", "已禁用"); + + private final String code; + + private final String description; + + FormDefinitionStatusEnums(String code, String description) { + this.code = code; + this.description = description; + } + +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDataRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDataRepository.java new file mode 100644 index 00000000..4e32245c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDataRepository.java @@ -0,0 +1,65 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.FormData; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 表单数据Repository接口 + * + * @author qqchen + * @date 2025-10-24 + */ +@Repository +public interface IFormDataRepository extends IBaseRepository { + + /** + * 根据业务标识查询 + * + * @param businessKey 业务标识 + * @return 表单数据 + */ + Optional findByBusinessKeyAndDeletedFalse(String businessKey); + + /** + * 根据表单标识分页查询 + * + * @param formKey 表单标识 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByFormKeyAndDeletedFalse(String formKey, Pageable pageable); + + /** + * 根据提交人分页查询 + * + * @param submitter 提交人 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findBySubmitterAndDeletedFalse(String submitter, Pageable pageable); + + /** + * 根据状态分页查询 + * + * @param status 状态 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByStatusAndDeletedFalse(FormDataStatusEnums status, Pageable pageable); + + /** + * 根据表单定义ID查询列表 + * + * @param formDefinitionId 表单定义ID + * @return 表单数据列表 + */ + List findByFormDefinitionIdAndDeletedFalse(Long formDefinitionId); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDefinitionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDefinitionRepository.java new file mode 100644 index 00000000..2d74c3e5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormDefinitionRepository.java @@ -0,0 +1,64 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 表单定义Repository接口 + * + * @author qqchen + * @date 2025-10-24 + */ +@Repository +public interface IFormDefinitionRepository extends IBaseRepository { + + /** + * 根据表单标识和版本查询 + * + * @param key 表单标识 + * @param formVersion 表单版本 + * @return 表单定义 + */ + Optional findByKeyAndFormVersionAndDeletedFalse(String key, Integer formVersion); + + /** + * 根据表单标识查询最新版本 + * + * @param key 表单标识 + * @return 表单定义 + */ + Optional findFirstByKeyAndDeletedFalseOrderByFormVersionDesc(String key); + + /** + * 检查表单标识是否存在 + * + * @param key 表单标识 + * @return 是否存在 + */ + boolean existsByKeyAndDeletedFalse(String key); + + /** + * 根据状态分页查询 + * + * @param status 状态 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByStatusAndDeletedFalse(FormDefinitionStatusEnums status, Pageable pageable); + + /** + * 根据状态查询列表 + * + * @param status 状态 + * @return 表单定义列表 + */ + List findByStatusAndDeletedFalse(FormDefinitionStatusEnums status); +} + 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 new file mode 100644 index 00000000..59a8a0a5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDataService.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +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; + +/** + * 表单数据服务接口 + * + * @author qqchen + * @date 2025-10-24 + */ +public interface IFormDataService extends IBaseService { + + /** + * 提交表单数据 + * + * @param dto 表单数据DTO + * @return 提交后的表单数据 + */ + FormDataDTO submit(FormDataDTO dto); + + /** + * 根据业务标识查询表单数据 + * + * @param businessKey 业务标识 + * @return 表单数据 + */ + FormDataDTO getByBusinessKey(String businessKey); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDefinitionService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDefinitionService.java new file mode 100644 index 00000000..863fb2e8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormDefinitionService.java @@ -0,0 +1,33 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; + +/** + * 表单定义服务接口 + * + * @author qqchen + * @date 2025-10-24 + */ +public interface IFormDefinitionService extends IBaseService { + + /** + * 发布表单(版本不可变) + * 如果当前表单已发布,则创建新版本;否则直接发布 + * + * @param id 表单定义ID + * @return 发布后的表单定义 + */ + FormDefinitionDTO publish(Long id); + + /** + * 根据表单标识获取最新版本 + * + * @param key 表单标识 + * @return 表单定义 + */ + FormDefinitionDTO getLatestByKey(String key); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDataServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDataServiceImpl.java new file mode 100644 index 00000000..6b8dcbb8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDataServiceImpl.java @@ -0,0 +1,74 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +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; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import com.qqchen.deploy.backend.workflow.enums.FormDataStatusEnums; +import com.qqchen.deploy.backend.workflow.repository.IFormDataRepository; +import com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository; +import com.qqchen.deploy.backend.workflow.service.IFormDataService; +import com.qqchen.deploy.backend.workflow.converter.FormDataConverter; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 表单数据服务实现 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@Service +public class FormDataServiceImpl extends BaseServiceImpl implements IFormDataService { + + @Resource + private IFormDataRepository formDataRepository; + + @Resource + private IFormDefinitionRepository formDefinitionRepository; + + @Resource + private FormDataConverter formDataConverter; + + @Override + @Transactional + public FormDataDTO submit(FormDataDTO dto) { + log.info("提交表单数据: formKey={}, submitter={}", dto.getFormKey(), dto.getSubmitter()); + + // 1. 查询表单定义,获取schema快照 + FormDefinition formDefinition = formDefinitionRepository.findById(dto.getFormDefinitionId()) + .orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND)); + + // 2. 创建表单数据 + FormData formData = formDataConverter.toEntity(dto); + formData.setFormKey(formDefinition.getKey()); + formData.setFormVersion(formDefinition.getFormVersion()); + formData.setSchemaSnapshot(formDefinition.getSchema()); // 保存schema快照 + formData.setSubmitTime(LocalDateTime.now()); + formData.setStatus(FormDataStatusEnums.SUBMITTED); + + // 3. 保存 + FormData savedFormData = formDataRepository.save(formData); + log.info("表单数据提交成功: id={}, formKey={}", savedFormData.getId(), savedFormData.getFormKey()); + + return formDataConverter.toDto(savedFormData); + } + + @Override + public FormDataDTO getByBusinessKey(String businessKey) { + log.info("根据业务标识查询表单数据: businessKey={}", businessKey); + + FormData formData = formDataRepository.findByBusinessKeyAndDeletedFalse(businessKey) + .orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DATA_NOT_FOUND)); + + return formDataConverter.toDto(formData); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java new file mode 100644 index 00000000..ac989144 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java @@ -0,0 +1,88 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.workflow.dto.FormDefinitionDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormDefinitionQuery; +import com.qqchen.deploy.backend.workflow.entity.FormDefinition; +import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; +import com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository; +import com.qqchen.deploy.backend.workflow.service.IFormDefinitionService; +import com.qqchen.deploy.backend.workflow.converter.FormDefinitionConverter; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 表单定义服务实现 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@Service +public class FormDefinitionServiceImpl extends BaseServiceImpl implements IFormDefinitionService { + + @Resource + private IFormDefinitionRepository formDefinitionRepository; + + @Resource + private FormDefinitionConverter formDefinitionConverter; + + @Override + @Transactional + public FormDefinitionDTO publish(Long id) { + log.info("发布表单定义,ID: {}", id); + + // 1. 查询当前表单定义 + FormDefinition currentForm = formDefinitionRepository.findById(id) + .orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND)); + + // 2. 检查是否已发布 + if (FormDefinitionStatusEnums.PUBLISHED.equals(currentForm.getStatus())) { + log.info("表单已发布,创建新版本: key={}, currentVersion={}", currentForm.getKey(), currentForm.getFormVersion()); + + // 创建新版本 + FormDefinition newVersion = new FormDefinition(); + newVersion.setName(currentForm.getName()); + newVersion.setKey(currentForm.getKey()); + newVersion.setFormVersion(currentForm.getFormVersion() + 1); + newVersion.setCategory(currentForm.getCategory()); + newVersion.setDescription(currentForm.getDescription()); + newVersion.setSchema(currentForm.getSchema()); + newVersion.setTags(currentForm.getTags()); + newVersion.setStatus(FormDefinitionStatusEnums.PUBLISHED); + newVersion.setIsTemplate(currentForm.getIsTemplate()); + + FormDefinition savedNewVersion = formDefinitionRepository.save(newVersion); + log.info("新版本创建成功: id={}, version={}", savedNewVersion.getId(), savedNewVersion.getFormVersion()); + + return formDefinitionConverter.toDto(savedNewVersion); + } else { + // 3. 首次发布,直接更新状态 + log.info("首次发布表单: key={}, version={}", currentForm.getKey(), currentForm.getFormVersion()); + currentForm.setStatus(FormDefinitionStatusEnums.PUBLISHED); + if (currentForm.getFormVersion() == null) { + currentForm.setFormVersion(1); + } + + FormDefinition savedForm = formDefinitionRepository.save(currentForm); + log.info("表单发布成功: id={}, version={}", savedForm.getId(), savedForm.getFormVersion()); + + return formDefinitionConverter.toDto(savedForm); + } + } + + @Override + public FormDefinitionDTO getLatestByKey(String key) { + log.info("根据key获取最新版本表单: key={}", key); + + FormDefinition formDefinition = formDefinitionRepository + .findFirstByKeyAndDeletedFalseOrderByFormVersionDesc(key) + .orElseThrow(() -> new BusinessException(com.qqchen.deploy.backend.framework.enums.ResponseCode.FORM_DEFINITION_NOT_FOUND)); + + return formDefinitionConverter.toDto(formDefinition); + } +} + diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index c5ac9400..574d6758 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -689,3 +689,91 @@ CREATE TABLE sys_notification_channel ( INDEX idx_deleted (deleted) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知渠道配置表'; +-- ===================================================================================================================== +-- 表单管理相关表 +-- ===================================================================================================================== + +-- 表单定义表 +CREATE TABLE form_definition +( + -- 主键 + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + + -- 基础信息 + name VARCHAR(255) NOT NULL COMMENT '表单名称', + `key` VARCHAR(255) NOT NULL COMMENT '表单标识(业务唯一)', + form_version INT NOT NULL DEFAULT 1 COMMENT '表单版本号', + category VARCHAR(100) NULL COMMENT '表单分类(APPROVAL-审批、DATA_COLLECTION-数据采集、SURVEY-问卷调查、OTHER-其他)', + description TEXT NULL COMMENT '表单描述', + + -- 表单配置 + schema JSON NOT NULL COMMENT '表单Schema(前端设计器导出的JSON结构)', + tags JSON NULL COMMENT '标签(用于分类和搜索)', + + -- 表单属性 + status VARCHAR(50) NOT NULL DEFAULT 'DRAFT' COMMENT '状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)', + is_template BIT NOT NULL DEFAULT 0 COMMENT '是否为模板', + + -- 审计字段 + create_by VARCHAR(255) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + update_by VARCHAR(255) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + + -- 约束和索引 + UNIQUE KEY uk_key_version (`key`, form_version), + INDEX idx_category (category), + INDEX idx_status (status), + INDEX idx_is_template (is_template), + INDEX idx_deleted (deleted) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = '表单定义表'; + +-- 表单数据表 +CREATE TABLE form_data +( + -- 主键 + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + + -- 关联表单定义 + form_definition_id BIGINT NOT NULL COMMENT '表单定义ID', + form_key VARCHAR(255) NOT NULL COMMENT '表单标识(冗余存储,避免JOIN)', + form_version INT NOT NULL COMMENT '表单版本(冗余存储,用于历史追溯)', + + -- 业务关联(松耦合) + business_key VARCHAR(255) NULL COMMENT '业务标识(如工作流实例ID、订单号等)', + business_type VARCHAR(50) NULL COMMENT '业务类型(WORKFLOW-工作流、ORDER-订单、STANDALONE-独立表单)', + + -- 表单数据 + data JSON NOT NULL COMMENT '表单填写数据(用户提交的实际数据)', + schema_snapshot JSON NOT NULL COMMENT '表单Schema快照(用于历史追溯,确保数据可还原)', + + -- 提交信息 + submitter VARCHAR(255) NULL COMMENT '提交人', + submit_time DATETIME(6) NULL COMMENT '提交时间', + + -- 状态 + status VARCHAR(50) NOT NULL DEFAULT 'SUBMITTED' COMMENT '状态(DRAFT-草稿、SUBMITTED-已提交、COMPLETED-已完成、REJECTED-已拒绝)', + + -- 审计字段 + create_by VARCHAR(255) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + update_by VARCHAR(255) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + + -- 索引 + INDEX idx_form_definition_id (form_definition_id), + INDEX idx_form_key (form_key), + INDEX idx_business_key (business_key), + INDEX idx_business_type (business_type), + INDEX idx_submitter (submitter), + INDEX idx_status (status), + INDEX idx_submit_time (submit_time), + INDEX idx_deleted (deleted) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = '表单数据表'; + From 951fcbfebb232353e5b6ce31b8057fa6df34c922 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 24 Oct 2025 12:43:05 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0form=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/FormCategoryApiController.java | 44 ++++++++++++++ .../workflow/api/FormDataApiController.java | 2 +- .../api/FormDefinitionApiController.java | 2 +- .../converter/FormCategoryConverter.java | 17 ++++++ .../backend/workflow/dto/FormCategoryDTO.java | 55 ++++++++++++++++++ .../workflow/dto/FormDefinitionDTO.java | 13 +++-- .../workflow/dto/query/FormCategoryQuery.java | 37 ++++++++++++ .../dto/query/FormDefinitionQuery.java | 7 +-- .../backend/workflow/entity/FormCategory.java | 58 +++++++++++++++++++ .../workflow/entity/FormDefinition.java | 10 ++-- .../workflow/enums/FormCategoryEnums.java | 29 ---------- .../repository/IFormCategoryRepository.java | 42 ++++++++++++++ .../service/IFormCategoryService.java | 25 ++++++++ .../service/impl/FormCategoryServiceImpl.java | 42 ++++++++++++++ .../impl/FormDefinitionServiceImpl.java | 2 +- .../db/changelog/changes/v1.0.0-data.sql | 39 ++++++++++++- .../db/changelog/changes/v1.0.0-schema.sql | 43 ++++++++++++-- .../src/main/resources/messages.properties | 4 ++ 18 files changed, 418 insertions(+), 53 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormCategoryApiController.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormCategoryConverter.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormCategoryDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormCategoryQuery.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormCategory.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormCategoryRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormCategoryService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormCategoryServiceImpl.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormCategoryApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormCategoryApiController.java new file mode 100644 index 00000000..7538d068 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormCategoryApiController.java @@ -0,0 +1,44 @@ +package com.qqchen.deploy.backend.workflow.api; + +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery; +import com.qqchen.deploy.backend.workflow.entity.FormCategory; +import com.qqchen.deploy.backend.workflow.service.IFormCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 表单分类API控制器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/forms/categories") +@Tag(name = "表单分类管理", description = "表单分类管理相关接口") +public class FormCategoryApiController extends BaseController { + + @Resource + private IFormCategoryService formCategoryService; + + @Operation(summary = "查询所有启用的分类") + @GetMapping("/enabled") + public Response> findAllEnabled() { + List result = formCategoryService.findAllEnabled(); + return Response.success(result); + } + + @Override + protected void exportData(jakarta.servlet.http.HttpServletResponse response, java.util.List data) { + // 暂不支持导出 + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java index 7adcabb8..984ed71b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDataApiController.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.*; */ @Slf4j @RestController -@RequestMapping("/api/v1/workflow/form-data") +@RequestMapping("/api/v1/forms/data") @Tag(name = "表单数据管理", description = "表单数据管理相关接口") public class FormDataApiController extends BaseController { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java index 81f44ee5..ee6259ea 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/FormDefinitionApiController.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.*; */ @Slf4j @RestController -@RequestMapping("/api/v1/workflow/form-definition") +@RequestMapping("/api/v1/forms/definitions") @Tag(name = "表单定义管理", description = "表单定义管理相关接口") public class FormDefinitionApiController extends BaseController { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormCategoryConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormCategoryConverter.java new file mode 100644 index 00000000..06cd087b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/FormCategoryConverter.java @@ -0,0 +1,17 @@ +package com.qqchen.deploy.backend.workflow.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO; +import com.qqchen.deploy.backend.workflow.entity.FormCategory; +import org.mapstruct.Mapper; + +/** + * 表单分类转换器 + * + * @author qqchen + * @date 2025-10-24 + */ +@Mapper(config = BaseConverter.class) +public interface FormCategoryConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormCategoryDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormCategoryDTO.java new file mode 100644 index 00000000..24ea2055 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormCategoryDTO.java @@ -0,0 +1,55 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 表单分类DTO + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "表单分类DTO") +public class FormCategoryDTO extends BaseDTO { + + /** + * 分类名称 + */ + @Schema(description = "分类名称", example = "审批表单") + private String name; + + /** + * 分类编码 + */ + @Schema(description = "分类编码", example = "APPROVAL") + private String code; + + /** + * 分类描述 + */ + @Schema(description = "分类描述", example = "用于审批流程的表单") + private String description; + + /** + * 图标 + */ + @Schema(description = "图标", example = "CheckCircleOutlined") + private String icon; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 是否启用 + */ + @Schema(description = "是否启用", example = "true") + private Boolean enabled; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java index 47948755..f6e8b7da 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/FormDefinitionDTO.java @@ -2,7 +2,6 @@ package com.qqchen.deploy.backend.workflow.dto; import com.qqchen.deploy.backend.framework.dto.BaseDTO; import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; -import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -40,10 +39,16 @@ public class FormDefinitionDTO extends BaseDTO { private Integer formVersion; /** - * 表单分类 + * 表单分类ID */ - @Schema(description = "表单分类") - private FormCategoryEnums category; + @Schema(description = "表单分类ID", example = "1") + private Long categoryId; + + /** + * 表单分类信息(用于展示) + */ + @Schema(description = "表单分类信息") + private FormCategoryDTO category; /** * 表单描述 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormCategoryQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormCategoryQuery.java new file mode 100644 index 00000000..2f593fb6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormCategoryQuery.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.workflow.dto.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 表单分类查询对象 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FormCategoryQuery extends BaseQuery { + + /** + * 分类名称(模糊查询) + */ + @QueryField(field = "name", type = QueryType.LIKE) + private String name; + + /** + * 分类编码(精确查询) + */ + @QueryField(field = "code", type = QueryType.EQUAL) + private String code; + + /** + * 是否启用(精确查询) + */ + @QueryField(field = "enabled", type = QueryType.EQUAL) + private Boolean enabled; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java index 7e0a808d..4b8fa635 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/query/FormDefinitionQuery.java @@ -3,7 +3,6 @@ package com.qqchen.deploy.backend.workflow.dto.query; import com.qqchen.deploy.backend.framework.annotation.QueryField; import com.qqchen.deploy.backend.framework.enums.QueryType; import com.qqchen.deploy.backend.framework.query.BaseQuery; -import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; import lombok.Data; import lombok.EqualsAndHashCode; @@ -37,10 +36,10 @@ public class FormDefinitionQuery extends BaseQuery { private Integer formVersion; /** - * 表单分类(精确查询) + * 表单分类ID(精确查询) */ - @QueryField(field = "category", type = QueryType.EQUAL) - private FormCategoryEnums category; + @QueryField(field = "categoryId", type = QueryType.EQUAL) + private Long categoryId; /** * 状态(精确查询) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormCategory.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormCategory.java new file mode 100644 index 00000000..6fb220f1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormCategory.java @@ -0,0 +1,58 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 表单分类实体 + * + * @author qqchen + * @date 2025-10-24 + */ +@Data +@Table(name = "form_category") +@jakarta.persistence.Entity +@EqualsAndHashCode(callSuper = true) +@LogicDelete +public class FormCategory extends Entity { + + /** + * 分类名称 + */ + @Column(nullable = false, length = 100) + private String name; + + /** + * 分类编码(唯一) + */ + @Column(nullable = false, length = 50, unique = true) + private String code; + + /** + * 分类描述 + */ + @Column(length = 500) + private String description; + + /** + * 图标 + */ + @Column(length = 50) + private String icon; + + /** + * 排序 + */ + @Column(nullable = false) + private Integer sort; + + /** + * 是否启用 + */ + @Column(nullable = false) + private Boolean enabled; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java index 7ce04baa..9164b893 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/FormDefinition.java @@ -4,7 +4,6 @@ import com.qqchen.deploy.backend.framework.annotation.LogicDelete; import com.qqchen.deploy.backend.framework.domain.Entity; import com.qqchen.deploy.backend.workflow.dto.form.FormSchema; import com.qqchen.deploy.backend.workflow.entity.converter.FormSchemaType; -import com.qqchen.deploy.backend.workflow.enums.FormCategoryEnums; import com.qqchen.deploy.backend.workflow.enums.FormDefinitionStatusEnums; import com.vladmihalcea.hibernate.type.json.JsonType; import jakarta.persistence.*; @@ -46,11 +45,10 @@ public class FormDefinition extends Entity { private Integer formVersion; /** - * 表单分类 + * 表单分类ID */ - @Column(name = "category") - @Enumerated(EnumType.STRING) - private FormCategoryEnums category; + @Column(name = "category_id") + private Long categoryId; /** * 表单描述 @@ -62,7 +60,7 @@ public class FormDefinition extends Entity { * 表单Schema(前端设计器导出的JSON结构) */ @Type(FormSchemaType.class) - @Column(name = "schema", nullable = false, columnDefinition = "json") + @Column(name = "`schema`", nullable = false, columnDefinition = "json") private FormSchema schema; /** diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java deleted file mode 100644 index 1817121e..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/FormCategoryEnums.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.qqchen.deploy.backend.workflow.enums; - -import lombok.Getter; - -/** - * 表单分类枚举 - * - * @author qqchen - * @date 2025-10-24 - */ -@Getter -public enum FormCategoryEnums { - - APPROVAL("APPROVAL", "审批"), - DATA_COLLECTION("DATA_COLLECTION", "数据采集"), - SURVEY("SURVEY", "问卷调查"), - OTHER("OTHER", "其他"); - - private final String code; - - private final String description; - - FormCategoryEnums(String code, String description) { - this.code = code; - this.description = description; - } - -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormCategoryRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormCategoryRepository.java new file mode 100644 index 00000000..46b86b85 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IFormCategoryRepository.java @@ -0,0 +1,42 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.workflow.entity.FormCategory; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 表单分类Repository接口 + * + * @author qqchen + * @date 2025-10-24 + */ +@Repository +public interface IFormCategoryRepository extends IBaseRepository { + + /** + * 根据编码查询 + * + * @param code 分类编码 + * @return 表单分类 + */ + Optional findByCodeAndDeletedFalse(String code); + + /** + * 检查编码是否存在 + * + * @param code 分类编码 + * @return 是否存在 + */ + boolean existsByCodeAndDeletedFalse(String code); + + /** + * 查询所有启用的分类(按排序) + * + * @return 分类列表 + */ + List findByEnabledTrueAndDeletedFalseOrderBySortAsc(); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormCategoryService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormCategoryService.java new file mode 100644 index 00000000..fdb74a89 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IFormCategoryService.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.workflow.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery; +import com.qqchen.deploy.backend.workflow.entity.FormCategory; + +import java.util.List; + +/** + * 表单分类服务接口 + * + * @author qqchen + * @date 2025-10-24 + */ +public interface IFormCategoryService extends IBaseService { + + /** + * 查询所有启用的分类 + * + * @return 分类列表 + */ + List findAllEnabled(); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormCategoryServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormCategoryServiceImpl.java new file mode 100644 index 00000000..80f7c621 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormCategoryServiceImpl.java @@ -0,0 +1,42 @@ +package com.qqchen.deploy.backend.workflow.service.impl; + +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.workflow.dto.FormCategoryDTO; +import com.qqchen.deploy.backend.workflow.dto.query.FormCategoryQuery; +import com.qqchen.deploy.backend.workflow.entity.FormCategory; +import com.qqchen.deploy.backend.workflow.repository.IFormCategoryRepository; +import com.qqchen.deploy.backend.workflow.service.IFormCategoryService; +import com.qqchen.deploy.backend.workflow.converter.FormCategoryConverter; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 表单分类服务实现 + * + * @author qqchen + * @date 2025-10-24 + */ +@Slf4j +@Service +public class FormCategoryServiceImpl extends BaseServiceImpl implements IFormCategoryService { + + @Resource + private IFormCategoryRepository formCategoryRepository; + + @Resource + private FormCategoryConverter formCategoryConverter; + + @Override + public List findAllEnabled() { + log.info("查询所有启用的表单分类"); + List categories = formCategoryRepository.findByEnabledTrueAndDeletedFalseOrderBySortAsc(); + return categories.stream() + .map(formCategoryConverter::toDto) + .collect(Collectors.toList()); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java index ac989144..b2cbc353 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/FormDefinitionServiceImpl.java @@ -48,7 +48,7 @@ public class FormDefinitionServiceImpl extends BaseServiceImpl Date: Fri, 24 Oct 2025 13:10:02 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E8=A1=A8=E5=8D=95CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/ui/dropdown-menu.tsx | 199 ++++++ frontend/src/pages/Form/Category/service.ts | 46 ++ frontend/src/pages/Form/Category/types.ts | 40 ++ frontend/src/pages/Form/Data/Detail.tsx | 150 +++++ frontend/src/pages/Form/Data/index.tsx | 408 +++++++++++++ frontend/src/pages/Form/Data/service.ts | 53 ++ frontend/src/pages/Form/Data/types.ts | 70 +++ .../src/pages/Form/Definition/Designer.tsx | 263 ++++++++ frontend/src/pages/Form/Definition/index.tsx | 571 ++++++++++++++++++ frontend/src/pages/Form/Definition/service.ts | 58 ++ frontend/src/pages/Form/Definition/types.ts | 52 ++ frontend/src/router/index.tsx | 49 ++ 12 files changed, 1959 insertions(+) create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/pages/Form/Category/service.ts create mode 100644 frontend/src/pages/Form/Category/types.ts create mode 100644 frontend/src/pages/Form/Data/Detail.tsx create mode 100644 frontend/src/pages/Form/Data/index.tsx create mode 100644 frontend/src/pages/Form/Data/service.ts create mode 100644 frontend/src/pages/Form/Data/types.ts create mode 100644 frontend/src/pages/Form/Definition/Designer.tsx create mode 100644 frontend/src/pages/Form/Definition/index.tsx create mode 100644 frontend/src/pages/Form/Definition/service.ts create mode 100644 frontend/src/pages/Form/Definition/types.ts diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..e804bca1 --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,199 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/frontend/src/pages/Form/Category/service.ts b/frontend/src/pages/Form/Category/service.ts new file mode 100644 index 00000000..4b77289b --- /dev/null +++ b/frontend/src/pages/Form/Category/service.ts @@ -0,0 +1,46 @@ +import request from '@/utils/request'; +import type { Page } from '@/types/base'; +import type { + FormCategoryQuery, + FormCategoryResponse, + FormCategoryRequest, +} from './types'; + +const BASE_URL = '/api/v1/forms/categories'; + +/** + * 分页查询分类 + */ +export const getCategories = (params?: FormCategoryQuery) => + request.get>(`${BASE_URL}/page`, { params }); + +/** + * 查询分类详情 + */ +export const getCategoryById = (id: number) => + request.get(`${BASE_URL}/${id}`); + +/** + * 查询所有启用的分类(常用) + */ +export const getEnabledCategories = () => + request.get(`${BASE_URL}/enabled`); + +/** + * 创建分类 + */ +export const createCategory = (data: FormCategoryRequest) => + request.post(BASE_URL, data); + +/** + * 更新分类 + */ +export const updateCategory = (id: number, data: FormCategoryRequest) => + request.put(`${BASE_URL}/${id}`, data); + +/** + * 删除分类 + */ +export const deleteCategory = (id: number) => + request.delete(`${BASE_URL}/${id}`); + diff --git a/frontend/src/pages/Form/Category/types.ts b/frontend/src/pages/Form/Category/types.ts new file mode 100644 index 00000000..7cb1b7c3 --- /dev/null +++ b/frontend/src/pages/Form/Category/types.ts @@ -0,0 +1,40 @@ +import { BaseQuery } from '@/types/base'; + +/** + * 表单分类查询参数 + */ +export interface FormCategoryQuery extends BaseQuery { + name?: string; + code?: string; + enabled?: boolean; +} + +/** + * 表单分类响应 + */ +export interface FormCategoryResponse { + id: number; + name: string; + code: string; + description?: string; + icon?: string; + sort: number; + enabled: boolean; + createBy?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; +} + +/** + * 表单分类创建/更新请求 + */ +export interface FormCategoryRequest { + name: string; + code: string; + description?: string; + icon?: string; + sort?: number; + enabled?: boolean; +} + diff --git a/frontend/src/pages/Form/Data/Detail.tsx b/frontend/src/pages/Form/Data/Detail.tsx new file mode 100644 index 00000000..82d545d2 --- /dev/null +++ b/frontend/src/pages/Form/Data/Detail.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { FormRenderer } from '@/components/FormDesigner'; +import { ArrowLeft, Loader2 } from 'lucide-react'; +import { getFormDataById } from './service'; +import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types'; + +/** + * 表单数据详情页 + */ +const FormDataDetail: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(null); + + // 加载数据 + useEffect(() => { + if (id) { + loadData(Number(id)); + } + }, [id]); + + const loadData = async (dataId: number) => { + setLoading(true); + try { + const result = await getFormDataById(dataId); + setData(result); + } catch (error) { + console.error('加载表单数据失败:', error); + } finally { + setLoading(false); + } + }; + + // 返回列表 + const handleBack = () => { + navigate('/form/data'); + }; + + // 状态徽章 + const getStatusBadge = (status: FormDataStatus) => { + const statusMap: Record = { + DRAFT: { variant: 'outline', text: '草稿' }, + SUBMITTED: { variant: 'success', text: '已提交' }, + COMPLETED: { variant: 'default', text: '已完成' }, + }; + const statusInfo = statusMap[status]; + return {statusInfo.text}; + }; + + // 业务类型徽章 + const getBusinessTypeBadge = (type: FormDataBusinessType) => { + const typeMap: Record = { + STANDALONE: { variant: 'outline', text: '独立' }, + WORKFLOW: { variant: 'default', text: '工作流' }, + ORDER: { variant: 'secondary', text: '订单' }, + }; + const typeInfo = typeMap[type]; + return {typeInfo.text}; + }; + + // 描述项组件 + const DescriptionItem: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +
+
{label}
+
{value}
+
+ ); + + if (loading) { + return ( +
+ + +
+ + 加载中... +
+
+
+
+ ); + } + + if (!data) { + return ( +
+ + +
数据不存在
+
+
+
+ ); + } + + return ( +
+ + +
+ 表单数据详情 + +
+
+ +
+ + + + + + + + + +
+
+
+ + + + 表单数据 + + + {/* 使用 FormRenderer 以只读模式展示数据 */} + + + +
+ ); +}; + +export default FormDataDetail; + diff --git a/frontend/src/pages/Form/Data/index.tsx b/frontend/src/pages/Form/Data/index.tsx new file mode 100644 index 00000000..daff70ba --- /dev/null +++ b/frontend/src/pages/Form/Data/index.tsx @@ -0,0 +1,408 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { DataTablePagination } from '@/components/ui/pagination'; +import { + Loader2, Search, Eye, Trash2, Download, Folder, + Activity, Clock, CheckCircle2, FileCheck, Database +} from 'lucide-react'; +import { getFormDataList, deleteFormData, exportFormData } from './service'; +import { getEnabledCategories } from '../Category/service'; +import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types'; +import type { FormCategoryResponse } from '../Category/types'; +import type { Page } from '@/types/base'; +import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import dayjs from 'dayjs'; + +/** + * 表单数据列表页 + */ +const FormDataList: React.FC = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [categories, setCategories] = useState([]); + const [query, setQuery] = useState({ + pageNum: DEFAULT_CURRENT - 1, + pageSize: DEFAULT_PAGE_SIZE, + formDefinitionId: searchParams.get('formDefinitionId') ? Number(searchParams.get('formDefinitionId')) : undefined, + businessKey: '', + categoryId: undefined as number | undefined, + status: undefined as FormDataStatus | undefined, + businessType: undefined as FormDataBusinessType | undefined, + }); + + // 加载分类列表 + const loadCategories = async () => { + try { + const result = await getEnabledCategories(); + setCategories(result || []); + } catch (error) { + console.error('加载分类失败:', error); + } + }; + + // 加载数据 + const loadData = async () => { + setLoading(true); + try { + const result = await getFormDataList(query); + setData(result); + } catch (error) { + console.error('加载表单数据失败:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadCategories(); + }, []); + + useEffect(() => { + loadData(); + }, [query]); + + // 搜索 + const handleSearch = () => { + setQuery(prev => ({ + ...prev, + pageNum: 0, + })); + }; + + // 重置搜索 + const handleReset = () => { + setQuery({ + pageNum: 0, + pageSize: DEFAULT_PAGE_SIZE, + formDefinitionId: undefined, + businessKey: '', + categoryId: undefined, + status: undefined, + businessType: undefined, + }); + }; + + // 根据分类 ID 获取分类信息 + const getCategoryInfo = (categoryId?: number) => { + return categories.find(cat => cat.id === categoryId); + }; + + // 查看详情 + const handleView = (record: FormDataResponse) => { + navigate(`/form/data/${record.id}`); + }; + + // 删除 + const handleDelete = async (record: FormDataResponse) => { + if (!confirm('确定要删除该数据吗?')) return; + try { + await deleteFormData(record.id); + loadData(); + } catch (error) { + console.error('删除数据失败:', error); + } + }; + + // 导出 + const handleExport = async () => { + try { + await exportFormData(query); + } catch (error) { + console.error('导出数据失败:', error); + } + }; + + // 状态徽章 + const getStatusBadge = (status: FormDataStatus) => { + const statusMap: Record = { + DRAFT: { variant: 'outline', text: '草稿', icon: Clock }, + SUBMITTED: { variant: 'success', text: '已提交', icon: CheckCircle2 }, + COMPLETED: { variant: 'default', text: '已完成', icon: FileCheck }, + }; + const statusInfo = statusMap[status]; + const Icon = statusInfo.icon; + return ( + + + {statusInfo.text} + + ); + }; + + // 业务类型徽章 + const getBusinessTypeBadge = (type: FormDataBusinessType) => { + const typeMap: Record = { + STANDALONE: { variant: 'outline', text: '独立' }, + WORKFLOW: { variant: 'default', text: '工作流' }, + ORDER: { variant: 'secondary', text: '订单' }, + }; + const typeInfo = typeMap[type]; + return {typeInfo.text}; + }; + + // 统计数据 + const stats = useMemo(() => { + const total = data?.totalElements || 0; + const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0; + const submittedCount = data?.content?.filter(d => d.status === 'SUBMITTED').length || 0; + const completedCount = data?.content?.filter(d => d.status === 'COMPLETED').length || 0; + return { total, draftCount, submittedCount, completedCount }; + }, [data]); + + const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0; + + return ( +
+
+

表单数据管理

+

+ 查看和管理用户提交的表单数据,支持按分类、状态、业务类型筛选。 +

+
+ + {/* 统计卡片 */} +
+ + + 总数据量 + + + +
{stats.total}
+

全部表单数据

+
+
+ + + 草稿 + + + +
{stats.draftCount}
+

暂存的数据

+
+
+ + + 已提交 + + + +
{stats.submittedCount}
+

待处理的数据

+
+
+ + + 已完成 + + + +
{stats.completedCount}
+

已处理完成

+
+
+
+ + + + 数据列表 + + + {/* 搜索栏 */} +
+
+ setQuery(prev => ({ ...prev, businessKey: e.target.value }))} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="h-9" + /> +
+ + + + + + +
+ + {/* 表格 */} +
+ + + + 表单标识 + 分类 + 业务类型 + 业务标识 + 提交人 + 提交时间 + 状态 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data?.content && data.content.length > 0 ? ( + data.content.map((record) => { + const categoryInfo = getCategoryInfo(record.categoryId); + return ( + + + + {record.formKey} + + + + {categoryInfo ? ( + + {categoryInfo.icon && } + {categoryInfo.name} + + ) : ( + 未分类 + )} + + {getBusinessTypeBadge(record.businessType)} + + {record.businessKey || '-'} + + + {record.submitter || '匿名'} + + + + {record.submitTime ? dayjs(record.submitTime).format('YYYY-MM-DD HH:mm:ss') : '-'} + + + {getStatusBadge(record.status)} + +
+ + +
+
+
+ ); + }) + ) : ( + + +
+ +
暂无表单数据
+
用户提交的表单数据将在此显示。
+
+
+
+ )} +
+
+
+ + {/* 分页 */} + {pageCount > 1 && ( + setQuery(prev => ({ + ...prev, + pageNum: page - 1 + }))} + /> + )} +
+
+
+ ); +}; + +export default FormDataList; + diff --git a/frontend/src/pages/Form/Data/service.ts b/frontend/src/pages/Form/Data/service.ts new file mode 100644 index 00000000..439877c1 --- /dev/null +++ b/frontend/src/pages/Form/Data/service.ts @@ -0,0 +1,53 @@ +import request from '@/utils/request'; +import type { Page } from '@/types/base'; +import type { + FormDataQuery, + FormDataResponse, + FormDataSubmitRequest, + FormDataUpdateRequest, +} from './types'; + +const BASE_URL = '/api/v1/forms/data'; + +/** + * 分页查询表单数据列表 + */ +export const getFormDataList = (params?: FormDataQuery) => + request.get>(`${BASE_URL}/page`, { params }); + +/** + * 获取表单数据详情 + */ +export const getFormDataById = (id: number) => + request.get(`${BASE_URL}/${id}`); + +/** + * 提交表单数据 + */ +export const submitFormData = (data: FormDataSubmitRequest) => + request.post(BASE_URL, data); + +/** + * 更新表单数据 + */ +export const updateFormData = (id: number, data: FormDataUpdateRequest) => + request.put(`${BASE_URL}/${id}`, data); + +/** + * 删除表单数据 + */ +export const deleteFormData = (id: number) => + request.delete(`${BASE_URL}/${id}`); + +/** + * 批量删除表单数据 + */ +export const batchDeleteFormData = (ids: number[]) => + request.post(`${BASE_URL}/batch-delete`, { ids }); + +/** + * 导出表单数据 + */ +export const exportFormData = (params?: FormDataQuery) => + request.download(`${BASE_URL}/export`, undefined, { params }); + diff --git a/frontend/src/pages/Form/Data/types.ts b/frontend/src/pages/Form/Data/types.ts new file mode 100644 index 00000000..28323cad --- /dev/null +++ b/frontend/src/pages/Form/Data/types.ts @@ -0,0 +1,70 @@ +import { BaseQuery } from '@/types/base'; +import type { FormSchema } from '@/components/FormDesigner'; + +/** + * 业务类型 + */ +export type FormDataBusinessType = 'STANDALONE' | 'WORKFLOW' | 'ORDER'; + +/** + * 表单数据状态 + */ +export type FormDataStatus = 'DRAFT' | 'SUBMITTED' | 'COMPLETED'; + +/** + * 表单数据查询参数 + */ +export interface FormDataQuery extends BaseQuery { + formDefinitionId?: number; + formKey?: string; + categoryId?: number; // 分类ID(筛选) + businessType?: FormDataBusinessType; + businessKey?: string; + submitter?: string; + status?: FormDataStatus; + submitTimeStart?: string; + submitTimeEnd?: string; +} + +/** + * 表单数据响应 + */ +export interface FormDataResponse { + id: number; + formDefinitionId: number; + formKey: string; + formVersion: number; + categoryId?: number; // 分类ID + businessKey?: string; + businessType: FormDataBusinessType; + data: Record; + schemaSnapshot: FormSchema; + submitter?: string; + submitTime?: string; + status: FormDataStatus; + createBy?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; +} + +/** + * 表单数据提交请求 + */ +export interface FormDataSubmitRequest { + formDefinitionId: number; + formKey: string; + businessType?: FormDataBusinessType; + businessKey?: string; + data: Record; + status?: FormDataStatus; +} + +/** + * 表单数据更新请求 + */ +export interface FormDataUpdateRequest { + data: Record; + status?: FormDataStatus; +} + diff --git a/frontend/src/pages/Form/Definition/Designer.tsx b/frontend/src/pages/Form/Definition/Designer.tsx new file mode 100644 index 00000000..2ea229b3 --- /dev/null +++ b/frontend/src/pages/Form/Definition/Designer.tsx @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { FormDesigner } from '@/components/FormDesigner'; +import type { FormSchema } from '@/components/FormDesigner'; +import { ArrowLeft, FileText, Tag, Folder, AlignLeft, Info } from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import { getDefinitionById, createDefinition, updateDefinition } from './service'; +import { getEnabledCategories } from '../Category/service'; +import type { FormDefinitionRequest } from './types'; +import type { FormCategoryResponse } from '../Category/types'; + +/** + * 表单设计器页面 + */ +const FormDesignerPage: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEdit = !!id; + + const [categories, setCategories] = useState([]); + const [formMeta, setFormMeta] = useState({ + name: '', + key: '', + categoryId: undefined as number | undefined, + description: '', + }); + const [formSchema, setFormSchema] = useState(null); + + // 加载分类列表 + const loadCategories = async () => { + try { + const result = await getEnabledCategories(); + setCategories(result || []); + } catch (error) { + console.error('加载分类失败:', error); + } + }; + + // 加载表单定义 + useEffect(() => { + loadCategories(); + if (isEdit && id) { + loadFormDefinition(Number(id)); + } + }, [id, isEdit]); + + const loadFormDefinition = async (definitionId: number) => { + try { + const result = await getDefinitionById(definitionId); + setFormMeta({ + name: result.name, + key: result.key, + categoryId: result.categoryId, + description: result.description || '', + }); + setFormSchema(result.schema); + } catch (error) { + console.error('加载表单定义失败:', error); + } + }; + + // 保存表单 + const handleSave = async (schema: FormSchema) => { + if (!formMeta.name.trim()) { + alert('请输入表单名称'); + return; + } + if (!formMeta.key.trim()) { + alert('请输入表单标识'); + return; + } + + const request: FormDefinitionRequest = { + name: formMeta.name, + key: formMeta.key, + categoryId: formMeta.categoryId, + description: formMeta.description, + schema, + status: 'PUBLISHED', + }; + + try { + if (isEdit && id) { + await updateDefinition(Number(id), request); + } else { + await createDefinition(request); + } + navigate('/form/definitions'); + } catch (error) { + console.error('保存表单失败:', error); + } + }; + + // 返回列表 + const handleBack = () => { + navigate('/form/definitions'); + }; + + return ( +
+ {/* 页面标题 */} +
+
+

+ {isEdit ? '编辑表单定义' : '创建表单定义'} +

+

+ {isEdit ? '修改表单的基本信息和字段配置' : '设计您的自定义表单,添加字段并配置验证规则'} +

+
+ +
+ + {/* 基本信息卡片 */} + + +
+
+ +
+
+ 基本信息 + + 设置表单的名称、标识和分类信息 + +
+
+
+ + +
+ {/* 第一行:表单名称 + 表单标识 */} +
+
+ + setFormMeta(prev => ({ ...prev, name: e.target.value }))} + className="h-10" + /> +

+ 将显示在表单列表和表单顶部 +

+
+ +
+ + setFormMeta(prev => ({ ...prev, key: e.target.value }))} + disabled={isEdit} + className="h-10 font-mono" + /> +

+ 英文字母、数字和中划线,用于 API 调用 +

+
+
+ + {/* 第二行:分类 */} +
+ + +

+ 帮助用户快速找到相关表单 +

+
+ + {/* 第三行:描述(跨整行) */} +
+ +