feat(用例管理): 创建用例接口

This commit is contained in:
WangXu10 2023-10-24 14:50:27 +08:00 committed by f2c-ci-robot[bot]
parent 1d6a8b4141
commit b6d9a1214d
32 changed files with 804 additions and 99 deletions

View File

@ -42,7 +42,7 @@ public class FunctionalCase implements Serializable {
@Size(min = 1, max = 255, message = "{functional_case.name.length_range}", groups = {Created.class, Updated.class})
private String name;
@Schema(description = "评审状态:未开始/进行中/已完成/已结束", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "评审状态:未评审/评审中/通过/不通过/重新提审", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{functional_case.review_status.not_blank}", groups = {Created.class})
@Size(min = 1, max = 64, message = "{functional_case.review_status.length_range}", groups = {Created.class, Updated.class})
private String reviewStatus;

View File

@ -25,16 +25,12 @@ public class FunctionalCaseCustomField implements Serializable {
@Schema(description = "字段值")
private String value;
@Schema(description = "富文本类型字段值")
private String textValue;
private static final long serialVersionUID = 1L;
public enum Column {
caseId("case_id", "caseId", "VARCHAR", false),
fieldId("field_id", "fieldId", "VARCHAR", false),
value("value", "value", "VARCHAR", true),
textValue("text_value", "textValue", "LONGVARCHAR", false);
value("value", "value", "VARCHAR", true);
private static final String BEGINNING_DELIMITER = "`";

View File

@ -16,22 +16,16 @@ public interface FunctionalCaseCustomFieldMapper {
int insertSelective(FunctionalCaseCustomField record);
List<FunctionalCaseCustomField> selectByExampleWithBLOBs(FunctionalCaseCustomFieldExample example);
List<FunctionalCaseCustomField> selectByExample(FunctionalCaseCustomFieldExample example);
FunctionalCaseCustomField selectByPrimaryKey(@Param("caseId") String caseId, @Param("fieldId") String fieldId);
int updateByExampleSelective(@Param("record") FunctionalCaseCustomField record, @Param("example") FunctionalCaseCustomFieldExample example);
int updateByExampleWithBLOBs(@Param("record") FunctionalCaseCustomField record, @Param("example") FunctionalCaseCustomFieldExample example);
int updateByExample(@Param("record") FunctionalCaseCustomField record, @Param("example") FunctionalCaseCustomFieldExample example);
int updateByPrimaryKeySelective(FunctionalCaseCustomField record);
int updateByPrimaryKeyWithBLOBs(FunctionalCaseCustomField record);
int updateByPrimaryKey(FunctionalCaseCustomField record);
int batchInsert(@Param("list") List<FunctionalCaseCustomField> list);

View File

@ -6,9 +6,6 @@
<id column="field_id" jdbcType="VARCHAR" property="fieldId" />
<result column="value" jdbcType="VARCHAR" property="value" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.functional.domain.FunctionalCaseCustomField">
<result column="text_value" jdbcType="LONGVARCHAR" property="textValue" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
@ -70,25 +67,6 @@
<sql id="Base_Column_List">
case_id, field_id, `value`
</sql>
<sql id="Blob_Column_List">
text_value
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomFieldExample" resultMap="ResultMapWithBLOBs">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from functional_case_custom_field
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByExample" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomFieldExample" resultMap="BaseResultMap">
select
<if test="distinct">
@ -103,11 +81,9 @@
order by ${orderByClause}
</if>
</select>
<select id="selectByPrimaryKey" parameterType="map" resultMap="ResultMapWithBLOBs">
<select id="selectByPrimaryKey" parameterType="map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from functional_case_custom_field
where case_id = #{caseId,jdbcType=VARCHAR}
and field_id = #{fieldId,jdbcType=VARCHAR}
@ -124,10 +100,10 @@
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomField">
insert into functional_case_custom_field (case_id, field_id, `value`,
text_value)
values (#{caseId,jdbcType=VARCHAR}, #{fieldId,jdbcType=VARCHAR}, #{value,jdbcType=VARCHAR},
#{textValue,jdbcType=LONGVARCHAR})
insert into functional_case_custom_field (case_id, field_id, `value`
)
values (#{caseId,jdbcType=VARCHAR}, #{fieldId,jdbcType=VARCHAR}, #{value,jdbcType=VARCHAR}
)
</insert>
<insert id="insertSelective" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomField">
insert into functional_case_custom_field
@ -141,9 +117,6 @@
<if test="value != null">
`value`,
</if>
<if test="textValue != null">
text_value,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="caseId != null">
@ -155,9 +128,6 @@
<if test="value != null">
#{value,jdbcType=VARCHAR},
</if>
<if test="textValue != null">
#{textValue,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomFieldExample" resultType="java.lang.Long">
@ -178,24 +148,11 @@
<if test="record.value != null">
`value` = #{record.value,jdbcType=VARCHAR},
</if>
<if test="record.textValue != null">
text_value = #{record.textValue,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExampleWithBLOBs" parameterType="map">
update functional_case_custom_field
set case_id = #{record.caseId,jdbcType=VARCHAR},
field_id = #{record.fieldId,jdbcType=VARCHAR},
`value` = #{record.value,jdbcType=VARCHAR},
text_value = #{record.textValue,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExample" parameterType="map">
update functional_case_custom_field
set case_id = #{record.caseId,jdbcType=VARCHAR},
@ -211,20 +168,10 @@
<if test="value != null">
`value` = #{value,jdbcType=VARCHAR},
</if>
<if test="textValue != null">
text_value = #{textValue,jdbcType=LONGVARCHAR},
</if>
</set>
where case_id = #{caseId,jdbcType=VARCHAR}
and field_id = #{fieldId,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomField">
update functional_case_custom_field
set `value` = #{value,jdbcType=VARCHAR},
text_value = #{textValue,jdbcType=LONGVARCHAR}
where case_id = #{caseId,jdbcType=VARCHAR}
and field_id = #{fieldId,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.functional.domain.FunctionalCaseCustomField">
update functional_case_custom_field
set `value` = #{value,jdbcType=VARCHAR}
@ -233,11 +180,11 @@
</update>
<insert id="batchInsert" parameterType="map">
insert into functional_case_custom_field
(case_id, field_id, `value`, text_value)
(case_id, field_id, `value`)
values
<foreach collection="list" item="item" separator=",">
(#{item.caseId,jdbcType=VARCHAR}, #{item.fieldId,jdbcType=VARCHAR}, #{item.value,jdbcType=VARCHAR},
#{item.textValue,jdbcType=LONGVARCHAR})
(#{item.caseId,jdbcType=VARCHAR}, #{item.fieldId,jdbcType=VARCHAR}, #{item.value,jdbcType=VARCHAR}
)
</foreach>
</insert>
<insert id="batchInsertSelective" parameterType="map">
@ -259,9 +206,6 @@
<if test="'value'.toString() == column.value">
#{item.value,jdbcType=VARCHAR}
</if>
<if test="'text_value'.toString() == column.value">
#{item.textValue,jdbcType=LONGVARCHAR}
</if>
</foreach>
)
</foreach>

View File

@ -8,13 +8,13 @@ CREATE TABLE IF NOT EXISTS functional_case(
`project_id` VARCHAR(50) NOT NULL COMMENT '项目ID' ,
`template_id` VARCHAR(50) NOT NULL COMMENT '模板ID' ,
`name` VARCHAR(255) NOT NULL COMMENT '名称' ,
`review_status` VARCHAR(64) NOT NULL DEFAULT 'PREPARE' COMMENT '评审状态:未开始/进行中/已完成/已结束' ,
`review_status` VARCHAR(64) NOT NULL DEFAULT 'UN_REVIEWED' COMMENT '评审状态:未评审/评审中/通过/不通过/重新提审' ,
`tags` VARCHAR(1000) COMMENT '标签JSON)' ,
`case_edit_type` VARCHAR(50) NOT NULL DEFAULT 'STEP' COMMENT '编辑模式:步骤模式/文本模式' ,
`pos` BIGINT NOT NULL DEFAULT 0 COMMENT '自定义排序间隔5000' ,
`version_id` VARCHAR(50) NOT NULL COMMENT '版本ID' ,
`ref_id` VARCHAR(50) NOT NULL COMMENT '指向初始版本ID' ,
`last_execute_result` VARCHAR(64) NOT NULL DEFAULT 'PREPARE' COMMENT '最近的执行结果:未执行/通过/失败/阻塞/跳过' ,
`last_execute_result` VARCHAR(64) NOT NULL DEFAULT 'UN_EXECUTED' COMMENT '最近的执行结果:未执行/通过/失败/阻塞/跳过' ,
`deleted` BIT(1) NOT NULL DEFAULT 0 COMMENT '是否在回收站0-否1-是' ,
`public_case` BIT(1) NOT NULL DEFAULT 0 COMMENT '是否是公共用例0-否1-是' ,
`latest` BIT(1) NOT NULL DEFAULT 0 COMMENT '是否为最新版本0-否1-是' ,

View File

@ -588,9 +588,8 @@ INSERT INTO message_task_blob(id, template) VALUES (@schedule_close_id, 'message
-- 初始化定时任务数据
SET @load_report_id = UUID_SHORT();
INSERT INTO schedule(`id`, `key`, `type`, `value`, `job`, `enable`, `resource_id`, `create_user`, `create_time`, `update_time`, `project_id`, `name`, `config`)
VALUES (@load_report_id, '100001100001', 'CRON', '0 0 2 * * ?', 'io.metersphere.project.job.CleanUpReportJob', true, '100001100001', 'admin', unix_timestamp() * 1000, unix_timestamp() * 1000, '100001100001', 'Clean Report Job', NULL);
VALUES (UUID_SHORT(), '100001100001', 'CRON', '0 0 2 * * ?', 'io.metersphere.project.job.CleanUpReportJob', true, '100001100001', 'admin', unix_timestamp() * 1000, unix_timestamp() * 1000, '100001100001', 'Clean Report Job', NULL);
-- 初始化默认项目版本配置项
INSERT INTO project_application (`project_id`, `type`, `type_value`) VALUES ('100001100001', 'VERSION_ENABLE', 'FALSE');

View File

@ -0,0 +1,9 @@
package io.metersphere.sdk.constants;
public enum FunctionalCaseExecuteResult {
UN_EXECUTED,
PASSED,
FAILED,
BLOCKED,
SKIPPED
}

View File

@ -0,0 +1,13 @@
package io.metersphere.sdk.constants;
/**
* @author wx
*/
public enum FunctionalCaseReviewStatus {
UN_REVIEWED,
UNDER_REVIEWED,
PASS,
UN_PASS,
RE_REVIEWED
}

View File

@ -207,4 +207,10 @@ public class PermissionConstants {
public static final String PROJECT_TEMPLATE_UPDATE = "PROJECT_TEMPLATE:READ+UPDATE";
public static final String PROJECT_TEMPLATE_DELETE = "PROJECT_TEMPLATE:READ+DELETE";
/*------ end: PROJECT_TEMPLATE ------*/
/*------ start: FUNCTIONAL_CASE ------*/
public static final String FUNCTIONAL_CASE_READ_ADD = "FUNCTIONAL_CASE:READ+ADD";
/*------ end: FUNCTIONAL_CASE ------*/
}

View File

@ -11,6 +11,9 @@ public class MsFileUtils {
public static final String PLUGIN_DIR_NAME = "plugins";
public static final String PLUGIN_DIR = DATE_ROOT_DIR + "/" + PLUGIN_DIR_NAME;
public static final String FUNCTIONAL_CASE_ATTACHMENT_DIR_NAME = "functionalCaseAttachment";
public static final String FUNCTIONAL_CASE_ATTACHMENT_DIR = DATE_ROOT_DIR + "/" + FUNCTIONAL_CASE_ATTACHMENT_DIR_NAME;
public static void validateFileName(String... fileNames) {
if (fileNames != null) {
for (String fileName : fileNames) {

View File

@ -80,6 +80,9 @@ functional_case_test.test_id.length_range=The length of the test ID must be betw
functional_case_test.test_id.not_blank=Test ID cannot be empty
functional_case_test.test_type.length_range=The length of the test type must be between 1 and 64
functional_case_test.test_type.not_blank=Test type cannot be empty
#FunctionalCaseCustomField
functional_case_custom_field.case_id.not_blank=Case ID cannot be empty
functional_case_custom_field.field_id.not_blank=Field ID cannot be empty
#moduleFunctionalCaseHistory
functional_case_history.id.not_blank=ID cannot be empty
functional_case_history.case_id.not_blank=Case ID cannot be empty
@ -131,4 +134,5 @@ case_review_follow.review_id.not_blank=Review ID cannot be empty
case_review_follow.follow_id.not_blank=follower cannot be empty
#moduleCustomFieldTestCase
custom_field_test_case.resource_id.not_blank=Resource ID cannot be empty
custom_field_test_case.field_id.not_blank=Field ID cannot be empty
custom_field_test_case.field_id.not_blank=Field ID cannot be empty
default_template_not_found=Default template not found

View File

@ -80,6 +80,9 @@ functional_case_test.test_id.length_range=其他类型用例ID长度必须在1-5
functional_case_test.test_id.not_blank=其他类型用例ID不能为空
functional_case_test.test_type.length_range=用例类型长度必须在1-64之间
functional_case_test.test_type.not_blank=用例类型不能为空
#FunctionalCaseCustomField
functional_case_custom_field.case_id.not_blank=功能用例ID不能为空
functional_case_custom_field.field_id.not_blank=自定义字段ID不能为空
#moduleFunctionalCaseHistory
functional_case_history.id.not_blank=ID不能为空
functional_case_history.case_id.not_blank=功能用例ID不能为空
@ -131,4 +134,5 @@ case_review_follow.review_id.not_blank=评审ID不能为空
case_review_follow.follow_id.not_blank=关注人不能为空
#moduleCustomFieldTestCase
custom_field_test_case.resource_id.not_blank=资源ID不能为空
custom_field_test_case.field_id.not_blank=字段ID不能为空
custom_field_test_case.field_id.not_blank=字段ID不能为空
default_template_not_found=默认模板不存在

View File

@ -80,6 +80,9 @@ functional_case_test.test_id.length_range=其他類型用例ID長度必須在1-5
functional_case_test.test_id.not_blank=其他類型用例ID不能為空
functional_case_test.test_type.length_range=用例類型長度必須在1-64之間
functional_case_test.test_type.not_blank=用例類型不能為空
#FunctionalCaseCustomField
functional_case_custom_field.case_id.not_blank=功能用例ID不能爲空
functional_case_custom_field.field_id.not_blank=自定義字段ID不能爲空
#moduleFunctionalCaseHistory
functional_case_history.id.not_blank=ID不能為空
functional_case_history.case_id.not_blank=功能用例ID不能爲空
@ -131,4 +134,5 @@ case_review_follow.review_id.not_blank=評審ID不能為空
case_review_follow.follow_id.not_blank=關注人不能為空
#moduleCustomFieldTestCase
custom_field_test_case.resource_id.not_blank=資源ID不能為空
custom_field_test_case.field_id.not_blank=字段ID不能為空
custom_field_test_case.field_id.not_blank=字段ID不能為空
default_template_not_found=默認模板不存在

View File

@ -28,6 +28,14 @@
<artifactId>metersphere-project-management</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>io.metersphere</groupId>
<artifactId>metersphere-system-setting</artifactId>
<version>${revision}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,44 @@
package io.metersphere.functional.controller;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.request.FunctionalCaseAddRequest;
import io.metersphere.functional.service.FunctionalCaseService;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @author wx
*/
@Tag(name = "功能测试-功能用例")
@RestController
@RequestMapping("/functional/case")
public class FunctionalCaseController {
@Resource
private FunctionalCaseService functionalCaseService;
//TODO 获取模板列表 获取对应模板自定义字段
@PostMapping("/add")
@Operation(summary = "功能用例-新增用例")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_ADD)
@Log(type = OperationLogType.ADD, expression = "#msClass.addFunctionalCaseLog(#request, #files)", msClass = FunctionalCaseService.class)
public FunctionalCase addFunctionalCase(@Validated @RequestPart("request") FunctionalCaseAddRequest request, @RequestPart(value = "files", required = false) List<MultipartFile> files) {
String userId = SessionUtils.getUserId();
return functionalCaseService.addFunctionalCase(request, files, userId);
}
}

View File

@ -0,0 +1,25 @@
package io.metersphere.functional.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author wx
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class CaseCustomsFieldDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "字段id")
@NotBlank(message = "{functional_case_custom_field.field_id.not_blank}")
private String fieldId;
@Schema(description = "自定义字段值")
private String value;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.functional.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class FileUploadDTO implements Serializable {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.functional.mapper;
import io.metersphere.functional.domain.FunctionalCase;
import org.apache.ibatis.annotations.Param;
/**
* @author wx
*/
public interface ExtFunctionalCaseMapper {
FunctionalCase getMaxNumByProjectId(@Param("projectId") String projectId);
Long getPos(@Param("projectId") String projectId);
;
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.functional.mapper.ExtFunctionalCaseMapper">
<select id="getMaxNumByProjectId" resultType="io.metersphere.functional.domain.FunctionalCase">
SELECT
num
FROM
functional_case
WHERE
project_id = #{projectId}
ORDER BY
num DESC
LIMIT 1;
</select>
<select id="getPos" resultType="java.lang.Long">
SELECT
pos
FROM
functional_case
WHERE
project_id = #{projectId}
ORDER BY
pos DESC
LIMIT 1;
</select>
</mapper>

View File

@ -0,0 +1,76 @@
package io.metersphere.functional.request;
import io.metersphere.functional.dto.CaseCustomsFieldDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* @author wx
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class FunctionalCaseAddRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "项目id")
@NotBlank(message = "{functional_case.project_id.not_blank}")
private String projectId;
@Schema(description = "模板id")
@NotBlank(message = "{functional_case.template_id.not_blank}")
private String templateId;
@Schema(description = "用例名称")
@NotBlank(message = "{functional_case.name.not_blank}")
private String name;
@Schema(description = "前置条件")
private String prerequisite;
@Schema(description = "编辑模式", allowableValues = {"STEP", "TEXT"})
@NotBlank(message = "{functional_case.case_edit_type.not_blank}")
private String caseEditType;
@Schema(description = "用例步骤")
private String steps;
@Schema(description = "步骤描述")
private String textDescription;
@Schema(description = "预期结果")
private String expectedResult;
@Schema(description = "备注")
private String description;
@Schema(description = "是否公共用例库")
private String publicCase;
@Schema(description = "模块id")
@NotBlank(message = "{functional_case.module_id.not_blank}")
private String moduleId;
@Schema(description = "版本id")
private String versionId;
@Schema(description = "标签")
private String tags;
@Schema(description = "自定义字段集合")
private List<CaseCustomsFieldDTO> customsFields;
@Schema(description = "关联文件ID集合")
private List<String> relateFileMetaIds = new ArrayList<>();
}

View File

@ -0,0 +1,26 @@
package io.metersphere.functional.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author wx
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class TemplateFieldsRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "模板id")
private String templateId;
@Schema(description = "项目id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{functional_case.project_id.not_blank}")
private String projectId;
}

View File

@ -1,14 +0,0 @@
package io.metersphere.functional.service;
import org.springframework.stereotype.Service;
/**
* 自定义字段功能用例关系表服务实现类
*
* @date : 2023-5-17
*/
@Service
public class CustomFieldTestCaseService {
}

View File

@ -1,10 +1,85 @@
package io.metersphere.functional.service;
import io.metersphere.functional.domain.FunctionalCaseAttachment;
import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper;
import io.metersphere.project.domain.FileMetadata;
import io.metersphere.project.mapper.FileMetadataMapper;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @author wx
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class FunctionalCaseAttachmentService {
@Resource
SqlSessionFactory sqlSessionFactory;
@Resource
private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper;
@Resource
private FileMetadataMapper fileMetadataMapper;
/**
* 保存本地上传文件和用例关联关系
*
* @param fileId
* @param file
* @param caseId
* @param isLocal
* @param userId
*/
public void saveCaseAttachment(String fileId, MultipartFile file, String caseId, Boolean isLocal, String userId) {
FunctionalCaseAttachment caseAttachment = creatModule(fileId, file.getName(), file.getSize(), caseId, isLocal, userId);
functionalCaseAttachmentMapper.insertSelective(caseAttachment);
}
/**
* 保存文件库文件与用例关联关系
*
* @param relateFileMetaIds
* @param caseId
* @param userId
*/
public void relateFileMeta(List<String> relateFileMetaIds, String caseId, String userId) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FunctionalCaseAttachmentMapper sessionMapper = sqlSession.getMapper(FunctionalCaseAttachmentMapper.class);
relateFileMetaIds.forEach(fileMetaId -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(fileMetaId);
FunctionalCaseAttachment caseAttachment = creatModule(fileMetadata.getId(), fileMetadata.getName(), fileMetadata.getSize(), caseId, false, userId);
sessionMapper.insertSelective(caseAttachment);
});
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
private FunctionalCaseAttachment creatModule(String fileId, String fileName, long fileSize, String caseId, Boolean isLocal, String userId) {
FunctionalCaseAttachment caseAttachment = new FunctionalCaseAttachment();
caseAttachment.setId(IDGenerator.nextStr());
caseAttachment.setCaseId(caseId);
caseAttachment.setFileId(fileId);
caseAttachment.setFileName(fileName);
caseAttachment.setSize(fileSize);
caseAttachment.setLocal(isLocal);
caseAttachment.setCreateUser(userId);
caseAttachment.setCreateTime(System.currentTimeMillis());
return caseAttachment;
}
}

View File

@ -0,0 +1,38 @@
package io.metersphere.functional.service;
import io.metersphere.functional.domain.FunctionalCaseCustomField;
import io.metersphere.functional.dto.CaseCustomsFieldDTO;
import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author wx
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class FunctionalCaseCustomFieldService {
@Resource
private FunctionalCaseCustomFieldMapper functionalCaseCustomFieldMapper;
/**
* 保存 用例-自定义字段关系
*
* @param customsFields
*/
public void saveCustomField(String caseId, List<CaseCustomsFieldDTO> customsFields) {
customsFields.forEach(customsField -> {
FunctionalCaseCustomField customField = new FunctionalCaseCustomField();
customField.setCaseId(caseId);
customField.setFieldId(customsField.getFieldId());
customField.setValue(customsField.getValue());
functionalCaseCustomFieldMapper.insertSelective(customField);
});
}
}

View File

@ -1,7 +1,174 @@
package io.metersphere.functional.service;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseBlob;
import io.metersphere.functional.dto.CaseCustomsFieldDTO;
import io.metersphere.functional.mapper.ExtFunctionalCaseMapper;
import io.metersphere.functional.mapper.FunctionalCaseBlobMapper;
import io.metersphere.functional.mapper.FunctionalCaseMapper;
import io.metersphere.functional.request.FunctionalCaseAddRequest;
import io.metersphere.sdk.constants.FunctionalCaseExecuteResult;
import io.metersphere.sdk.constants.FunctionalCaseReviewStatus;
import io.metersphere.sdk.constants.HttpMethodConstants;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.dto.LogDTO;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.MsFileUtils;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Optional;
/**
* @author wx
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class FunctionalCaseService {
public static final int ORDER_STEP = 5000;
@Resource
private MinioRepository minioRepository;
@Resource
private ExtFunctionalCaseMapper extFunctionalCaseMapper;
@Resource
private FunctionalCaseMapper functionalCaseMapper;
@Resource
private FunctionalCaseBlobMapper functionalCaseBlobMapper;
@Resource
private FunctionalCaseCustomFieldService functionalCaseCustomFieldService;
@Resource
private FunctionalCaseAttachmentService functionalCaseAttachmentService;
public FunctionalCase addFunctionalCase(FunctionalCaseAddRequest request, List<MultipartFile> files, String userId) {
String caseId = IDGenerator.nextStr();
//添加功能用例
FunctionalCase functionalCase = addTestCase(caseId, request, userId);
//上传文件
if (CollectionUtils.isNotEmpty(files)) {
uploadFile(request, caseId, files, true, userId);
}
//关联附件
if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) {
functionalCaseAttachmentService.relateFileMeta(request.getRelateFileMetaIds(), caseId, userId);
}
return functionalCase;
}
/**
* 添加功能用例
*
* @param request
*/
private FunctionalCase addTestCase(String caseId, FunctionalCaseAddRequest request, String userId) {
FunctionalCase functionalCase = new FunctionalCase();
BeanUtils.copyBean(functionalCase, request);
functionalCase.setId(caseId);
functionalCase.setNum(getNextNum(request.getProjectId()));
functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name());
functionalCase.setPos(getNextOrder(request.getProjectId()));
functionalCase.setRefId(caseId);
functionalCase.setLastExecuteResult(FunctionalCaseExecuteResult.UN_EXECUTED.name());
functionalCase.setLatest(true);
functionalCase.setCreateUser(userId);
functionalCase.setCreateTime(System.currentTimeMillis());
functionalCase.setUpdateTime(System.currentTimeMillis());
functionalCase.setVersionId(StringUtils.defaultIfBlank(request.getVersionId(), "v1.0.0"));
functionalCaseMapper.insertSelective(functionalCase);
//附属表
FunctionalCaseBlob functionalCaseBlob = new FunctionalCaseBlob();
functionalCaseBlob.setId(caseId);
BeanUtils.copyBean(functionalCaseBlob, request);
functionalCaseBlobMapper.insertSelective(functionalCaseBlob);
//保存自定义字段
List<CaseCustomsFieldDTO> customsFields = request.getCustomsFields();
if (CollectionUtils.isNotEmpty(customsFields)) {
functionalCaseCustomFieldService.saveCustomField(caseId, customsFields);
}
return functionalCase;
}
public Long getNextOrder(String projectId) {
Long pos = extFunctionalCaseMapper.getPos(projectId);
return (pos == null ? 0 : pos) + ORDER_STEP;
}
public int getNextNum(String projectId) {
//TODO 获取下一个num方法(暂时直接查询数据库)
FunctionalCase testCase = extFunctionalCaseMapper.getMaxNumByProjectId(projectId);
if (testCase == null || testCase.getNum() == null) {
return 100001;
} else {
return Optional.ofNullable(testCase.getNum() + 1).orElse(100001);
}
}
/**
* 功能用例上传附件
*
* @param request
* @param files
*/
public void uploadFile(FunctionalCaseAddRequest request, String caseId, List<MultipartFile> files, Boolean isLocal, String userId) {
files.forEach(file -> {
String fileId = IDGenerator.nextStr();
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(file.getName());
fileRequest.setProjectId(request.getProjectId());
fileRequest.setResourceId(MsFileUtils.FUNCTIONAL_CASE_ATTACHMENT_DIR + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
try {
minioRepository.saveFile(file, fileRequest);
} catch (Exception e) {
throw new MSException("save file error");
}
functionalCaseAttachmentService.saveCaseAttachment(fileId, file, caseId, isLocal, userId);
});
}
/**
* 新增用例 日志
*
* @param requests
* @param files
* @return
*/
public LogDTO addFunctionalCaseLog(FunctionalCaseAddRequest requests, List<MultipartFile> files) {
LogDTO dto = new LogDTO(
requests.getProjectId(),
null,
null,
null,
OperationLogType.ADD.name(),
OperationLogModule.FUNCTIONAL_CASE,
requests.getName());
dto.setPath("/functional/case/add");
dto.setMethod(HttpMethodConstants.POST.name());
dto.setOriginalValue(JSON.toJSONBytes(requests));
return dto;
}
}

View File

@ -0,0 +1,87 @@
package io.metersphere.functional.controller;
import io.metersphere.functional.dto.CaseCustomsFieldDTO;
import io.metersphere.functional.request.FunctionalCaseAddRequest;
import io.metersphere.functional.utils.FileBaseUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.handler.ResultHolder;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
public class FunctionalCaseControllerTests extends BaseTest {
public static final String FUNCTIONAL_CASE_URL = "/functional/case/add";
@Test
@Order(1)
@Sql(scripts = {"/dml/init_file_metadata_test.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
public void testTestPlanShare() throws Exception {
//新增
FunctionalCaseAddRequest request = creatFunctionalCase();
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
List<MockMultipartFile> files = new ArrayList<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("files", files);
MvcResult mvcResult = this.requestMultipartWithOkAndReturn(FUNCTIONAL_CASE_URL, paramMap);
// 获取返回值
String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class);
// 返回请求正常
Assertions.assertNotNull(resultHolder);
//设置自定义字段
List<CaseCustomsFieldDTO> dtoList = creatCustomsFields();
request.setCustomsFields(dtoList);
//设置文件
String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/test.JPG")).getPath();
MockMultipartFile file = new MockMultipartFile("file", "file_re-upload.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath));
files.add(file);
//设置关联文件
request.setRelateFileMetaIds(Arrays.asList("relate_file_meta_id_1", "relate_file_meta_id_2"));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("files", files);
this.requestMultipartWithOkAndReturn(FUNCTIONAL_CASE_URL, paramMap);
}
private List<CaseCustomsFieldDTO> creatCustomsFields() {
List<CaseCustomsFieldDTO> list = new ArrayList<>();
CaseCustomsFieldDTO customsFieldDTO = new CaseCustomsFieldDTO();
customsFieldDTO.setFieldId("customs_field_id_1");
customsFieldDTO.setValue("customs_field_value_1");
list.add(customsFieldDTO);
return list;
}
private FunctionalCaseAddRequest creatFunctionalCase() {
FunctionalCaseAddRequest functionalCaseAddRequest = new FunctionalCaseAddRequest();
functionalCaseAddRequest.setProjectId(DEFAULT_PROJECT_ID);
functionalCaseAddRequest.setTemplateId("default_template_id");
functionalCaseAddRequest.setName("测试用例新增");
functionalCaseAddRequest.setCaseEditType("STEP");
functionalCaseAddRequest.setModuleId("default_module_id");
return functionalCaseAddRequest;
}
}

View File

@ -0,0 +1,128 @@
package io.metersphere.functional.utils;
import io.metersphere.project.dto.FileInformationDTO;
import io.metersphere.project.request.filemanagement.FileMetadataTableRequest;
import io.metersphere.sdk.dto.BaseTreeNode;
import io.metersphere.sdk.util.FilePreviewUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Pager;
import org.apache.commons.collections4.CollectionUtils;
import org.junit.jupiter.api.Assertions;
import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.List;
import java.util.Map;
public class FileBaseUtils {
public static BaseTreeNode getNodeByName(List<BaseTreeNode> preliminaryTreeNodes, String nodeName) {
for (BaseTreeNode firstLevelNode : preliminaryTreeNodes) {
if (StringUtils.equals(firstLevelNode.getName(), nodeName)) {
return firstLevelNode;
}
if (CollectionUtils.isNotEmpty(firstLevelNode.getChildren())) {
for (BaseTreeNode secondLevelNode : firstLevelNode.getChildren()) {
if (StringUtils.equals(secondLevelNode.getName(), nodeName)) {
return secondLevelNode;
}
if (CollectionUtils.isNotEmpty(secondLevelNode.getChildren())) {
for (BaseTreeNode thirdLevelNode : secondLevelNode.getChildren()) {
if (StringUtils.equals(thirdLevelNode.getName(), nodeName)) {
return thirdLevelNode;
}
}
}
}
}
}
return null;
}
public static byte[] getFileBytes(String filePath) {
File file = new File(filePath);
byte[] buffer = new byte[0];
try (FileInputStream fi = new FileInputStream(file)) {
buffer = new byte[(int) file.length()];
int offset = 0;
int numRead;
while (offset < buffer.length
&& (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {
offset += numRead;
}
} catch (Exception ignore) {
}
return buffer;
}
public static String getFileMD5(File file) {
if (!file.isFile()) {
return null;
}
MessageDigest digest = null;
FileInputStream in = null;
byte buffer[] = new byte[8192];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer)) != -1) {
digest.update(buffer, 0, len);
}
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static String getFileMD5(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(bytes, 0, bytes.length);
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void checkFilePage(Pager<List<FileInformationDTO>> tableData, Map<String, Integer> moduleCount, FileMetadataTableRequest request, boolean hasData) {
//返回值的页码和当前页码相同
Assertions.assertEquals(tableData.getCurrent(), request.getCurrent());
//返回的数据量不超过规定要返回的数据量相同
Assertions.assertTrue(JSON.parseArray(JSON.toJSONString(tableData.getList())).size() <= request.getPageSize());
List<FileInformationDTO> fileInformationDTOList = JSON.parseArray(JSON.toJSONString(tableData.getList()), FileInformationDTO.class);
for (FileInformationDTO fileInformationDTO : fileInformationDTOList) {
if (FilePreviewUtils.isImage(fileInformationDTO.getFileType())) {
//检查是否有预览文件
String previewPath = fileInformationDTO.getPreviewSrc();
File file = new File(previewPath);
Assertions.assertTrue(file.exists());
}
}
//判断返回的节点统计总量是否和表格总量匹配
long allResult = 0;
for (int countByModuleId : moduleCount.values()) {
allResult += countByModuleId;
}
Assertions.assertEquals(allResult, tableData.getTotal());
Assertions.assertEquals(request.getPageSize(), tableData.getPageSize());
if (hasData) {
Assertions.assertTrue(allResult > 0);
} else {
Assertions.assertTrue(allResult == 0);
}
}
}

View File

@ -13,7 +13,7 @@ quartz.properties.org.quartz.jobStore.acquireTriggersWithinLock=true
#
logging.file.path=/opt/metersphere/logs/metersphere
# Hikari
spring.datasource.url=jdbc:mysql://${embedded.mysql.host}:${embedded.mysql.port}/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.url=jdbc:mysql://${embedded.mysql.host}:${embedded.mysql.port}/test?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&sessionVariables=sql_mode=%27STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION%27
spring.datasource.username=${embedded.mysql.user}
spring.datasource.password=${embedded.mysql.password}
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

View File

@ -0,0 +1,6 @@
INSERT INTO file_metadata(id, name, type, size, create_time, update_time, project_id, storage, create_user, update_user, tags, description, module_id, path, latest, ref_id, file_version) VALUES ('relate_file_meta_id_1', 'formItem', 'ts', 2502, 1698058347559, 1698058347559, '100001100001', 'MINIO', 'admin', 'admin', NULL, NULL, 'root', '100001100001/1127016598347779', b'1', '1127016598347779', '1127016598347779');
INSERT INTO file_metadata(id, name, type, size, create_time, update_time, project_id, storage, create_user, update_user, tags, description, module_id, path, latest, ref_id, file_version) VALUES ('relate_file_meta_id_2', 'formItem', 'ts', 2502, 1698058347559, 1698058347559, '100001100001', 'MINIO', 'admin', 'admin', NULL, NULL, 'root', '100001100001/1127016598347779', b'1', '1127016598347779', '1127016598347779');

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -44,7 +44,7 @@ import java.util.stream.Collectors;
import static io.metersphere.system.controller.handler.result.MsHttpResultCode.NOT_FOUND;
@Service
@Transactional
@Transactional(rollbackFor = Exception.class)
public class ProjectApplicationService {
@Resource
private ProjectApplicationMapper projectApplicationMapper;

View File

@ -94,4 +94,7 @@ public class OperationLogModule {
public static final String PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_ROBOT = "PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_ROBOT";
public static final String PROJECT_TEMPLATE = "PROJECT_TEMPLATE";// 项目模板
public static final String PROJECT_CUSTOM_FIELD = "PROJECT_CUSTOM_FIELD";// 项目字段
//用例
public static final String FUNCTIONAL_CASE = "FUNCTIONAL_CASE";
}