diff --git a/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java b/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java index 991f50d545..1b999337ce 100644 --- a/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java +++ b/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java @@ -2,8 +2,10 @@ package io.metersphere.listener; import io.metersphere.api.event.APIEventSource; import io.metersphere.plan.listener.ExecEventListener; +import io.metersphere.sdk.service.PluginLoadService; import io.metersphere.sdk.util.CommonBeanFactory; import io.metersphere.sdk.util.LogUtils; +import jakarta.annotation.Resource; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @@ -11,6 +13,9 @@ import org.springframework.stereotype.Component; @Component public class AppStartListener implements ApplicationRunner { + @Resource + private PluginLoadService pluginLoadService; + @Override public void run(ApplicationArguments args) throws Exception { LogUtils.info("================= 应用启动 ================="); @@ -32,5 +37,8 @@ public class AppStartListener implements ApplicationRunner { // 触发事件 apiEventSource.fireEvent("API", "Event after removing the listener test."); //loadEventSource.fireEvent("LOAD","Event after removing the listener."); + + // 加载插件 + pluginLoadService.loadPlugins(); } } diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java similarity index 79% rename from backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java rename to backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java index 6101cd698d..2641defa96 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java +++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java @@ -9,26 +9,30 @@ import java.util.Arrays; import lombok.Data; @Data -public class PluginFrontScript implements Serializable { +public class PluginScript implements Serializable { @Schema(title = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank(message = "{plugin_front_script.plugin_id.not_blank}", groups = {Created.class}) - @Size(min = 1, max = 50, message = "{plugin_front_script.plugin_id.length_range}", groups = {Created.class, Updated.class}) + @NotBlank(message = "{plugin_script.plugin_id.not_blank}", groups = {Created.class}) + @Size(min = 1, max = 50, message = "{plugin_script.plugin_id.length_range}", groups = {Created.class, Updated.class}) private String pluginId; @Schema(title = "插件中对应表单配置的ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank(message = "{plugin_front_script.script_id.not_blank}", groups = {Created.class}) - @Size(min = 1, max = 50, message = "{plugin_front_script.script_id.length_range}", groups = {Created.class, Updated.class}) + @NotBlank(message = "{plugin_script.script_id.not_blank}", groups = {Created.class}) + @Size(min = 1, max = 50, message = "{plugin_script.script_id.length_range}", groups = {Created.class, Updated.class}) private String scriptId; + @Schema(title = "插件中对应表单配置的名称") + private String name; + @Schema(title = "脚本内容") - private String script; + private byte[] script; private static final long serialVersionUID = 1L; public enum Column { pluginId("plugin_id", "pluginId", "VARCHAR", false), scriptId("script_id", "scriptId", "VARCHAR", false), - script("script", "script", "LONGVARCHAR", false); + name("name", "name", "VARCHAR", true), + script("script", "script", "LONGVARBINARY", false); private static final String BEGINNING_DELIMITER = "`"; diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java similarity index 80% rename from backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java rename to backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java index 3ab4ebfa04..bcd0df4dfd 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java +++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java @@ -3,14 +3,14 @@ package io.metersphere.system.domain; import java.util.ArrayList; import java.util.List; -public class PluginFrontScriptExample { +public class PluginScriptExample { protected String orderByClause; protected boolean distinct; protected List oredCriteria; - public PluginFrontScriptExample() { + public PluginScriptExample() { oredCriteria = new ArrayList(); } @@ -243,6 +243,76 @@ public class PluginFrontScriptExample { addCriterion("script_id not between", value1, value2, "scriptId"); return (Criteria) this; } + + public Criteria andNameIsNull() { + addCriterion("`name` is null"); + return (Criteria) this; + } + + public Criteria andNameIsNotNull() { + addCriterion("`name` is not null"); + return (Criteria) this; + } + + public Criteria andNameEqualTo(String value) { + addCriterion("`name` =", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotEqualTo(String value) { + addCriterion("`name` <>", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThan(String value) { + addCriterion("`name` >", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThanOrEqualTo(String value) { + addCriterion("`name` >=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThan(String value) { + addCriterion("`name` <", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThanOrEqualTo(String value) { + addCriterion("`name` <=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLike(String value) { + addCriterion("`name` like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotLike(String value) { + addCriterion("`name` not like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameIn(List values) { + addCriterion("`name` in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameNotIn(List values) { + addCriterion("`name` not in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameBetween(String value1, String value2) { + addCriterion("`name` between", value1, value2, "name"); + return (Criteria) this; + } + + public Criteria andNameNotBetween(String value1, String value2) { + addCriterion("`name` not between", value1, value2, "name"); + return (Criteria) this; + } } public static class Criteria extends GeneratedCriteria { diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java deleted file mode 100644 index 80199f3db0..0000000000 --- a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.metersphere.system.mapper; - -import io.metersphere.system.domain.PluginFrontScript; -import io.metersphere.system.domain.PluginFrontScriptExample; -import java.util.List; -import org.apache.ibatis.annotations.Param; - -public interface PluginFrontScriptMapper { - long countByExample(PluginFrontScriptExample example); - - int deleteByExample(PluginFrontScriptExample example); - - int deleteByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId); - - int insert(PluginFrontScript record); - - int insertSelective(PluginFrontScript record); - - List selectByExampleWithBLOBs(PluginFrontScriptExample example); - - List selectByExample(PluginFrontScriptExample example); - - PluginFrontScript selectByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId); - - int updateByExampleSelective(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example); - - int updateByExampleWithBLOBs(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example); - - int updateByExample(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example); - - int updateByPrimaryKeySelective(PluginFrontScript record); - - int updateByPrimaryKeyWithBLOBs(PluginFrontScript record); - - int batchInsert(@Param("list") List list); - - int batchInsertSelective(@Param("list") List list, @Param("selective") PluginFrontScript.Column ... selective); -} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java new file mode 100644 index 0000000000..0b3fe73413 --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java @@ -0,0 +1,40 @@ +package io.metersphere.system.mapper; + +import io.metersphere.system.domain.PluginScript; +import io.metersphere.system.domain.PluginScriptExample; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface PluginScriptMapper { + long countByExample(PluginScriptExample example); + + int deleteByExample(PluginScriptExample example); + + int deleteByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId); + + int insert(PluginScript record); + + int insertSelective(PluginScript record); + + List selectByExampleWithBLOBs(PluginScriptExample example); + + List selectByExample(PluginScriptExample example); + + PluginScript selectByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId); + + int updateByExampleSelective(@Param("record") PluginScript record, @Param("example") PluginScriptExample example); + + int updateByExampleWithBLOBs(@Param("record") PluginScript record, @Param("example") PluginScriptExample example); + + int updateByExample(@Param("record") PluginScript record, @Param("example") PluginScriptExample example); + + int updateByPrimaryKeySelective(PluginScript record); + + int updateByPrimaryKeyWithBLOBs(PluginScript record); + + int updateByPrimaryKey(PluginScript record); + + int batchInsert(@Param("list") List list); + + int batchInsertSelective(@Param("list") List list, @Param("selective") PluginScript.Column ... selective); +} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml similarity index 74% rename from backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml rename to backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml index 050e7d88bf..113298d515 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml +++ b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml @@ -1,12 +1,13 @@ - - + + + - - + + @@ -67,12 +68,12 @@ - plugin_id, script_id + plugin_id, script_id, `name` script - select distinct @@ -80,7 +81,7 @@ , - from plugin_front_script + from plugin_script @@ -88,13 +89,13 @@ order by ${orderByClause} - select distinct - from plugin_front_script + from plugin_script @@ -107,29 +108,29 @@ , - from plugin_front_script + from plugin_script where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - delete from plugin_front_script + delete from plugin_script where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - - delete from plugin_front_script + + delete from plugin_script - - insert into plugin_front_script (plugin_id, script_id, script - ) - values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{script,jdbcType=LONGVARCHAR} - ) + + insert into plugin_script (plugin_id, script_id, `name`, + script) + values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, + #{script,jdbcType=LONGVARBINARY}) - - insert into plugin_front_script + + insert into plugin_script plugin_id, @@ -137,6 +138,9 @@ script_id, + + `name`, + script, @@ -148,19 +152,22 @@ #{scriptId,jdbcType=VARCHAR}, + + #{name,jdbcType=VARCHAR}, + - #{script,jdbcType=LONGVARCHAR}, + #{script,jdbcType=LONGVARBINARY}, - + select count(*) from plugin_script - update plugin_front_script + update plugin_script plugin_id = #{record.pluginId,jdbcType=VARCHAR}, @@ -168,8 +175,11 @@ script_id = #{record.scriptId,jdbcType=VARCHAR}, + + `name` = #{record.name,jdbcType=VARCHAR}, + - script = #{record.script,jdbcType=LONGVARCHAR}, + script = #{record.script,jdbcType=LONGVARBINARY}, @@ -177,49 +187,61 @@ - update plugin_front_script + update plugin_script set plugin_id = #{record.pluginId,jdbcType=VARCHAR}, script_id = #{record.scriptId,jdbcType=VARCHAR}, - script = #{record.script,jdbcType=LONGVARCHAR} + `name` = #{record.name,jdbcType=VARCHAR}, + script = #{record.script,jdbcType=LONGVARBINARY} - update plugin_front_script + update plugin_script set plugin_id = #{record.pluginId,jdbcType=VARCHAR}, - script_id = #{record.scriptId,jdbcType=VARCHAR} + script_id = #{record.scriptId,jdbcType=VARCHAR}, + `name` = #{record.name,jdbcType=VARCHAR} - - update plugin_front_script + + update plugin_script + + `name` = #{name,jdbcType=VARCHAR}, + - script = #{script,jdbcType=LONGVARCHAR}, + script = #{script,jdbcType=LONGVARBINARY}, where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - - update plugin_front_script - set script = #{script,jdbcType=LONGVARCHAR} + + update plugin_script + set `name` = #{name,jdbcType=VARCHAR}, + script = #{script,jdbcType=LONGVARBINARY} + where plugin_id = #{pluginId,jdbcType=VARCHAR} + and script_id = #{scriptId,jdbcType=VARCHAR} + + + update plugin_script + set `name` = #{name,jdbcType=VARCHAR} where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - insert into plugin_front_script - (plugin_id, script_id, script) + insert into plugin_script + (plugin_id, script_id, `name`, script) values - (#{item.pluginId,jdbcType=VARCHAR}, #{item.scriptId,jdbcType=VARCHAR}, #{item.script,jdbcType=LONGVARCHAR} - ) + (#{item.pluginId,jdbcType=VARCHAR}, #{item.scriptId,jdbcType=VARCHAR}, #{item.name,jdbcType=VARCHAR}, + #{item.script,jdbcType=LONGVARBINARY}) - insert into plugin_front_script ( + insert into plugin_script ( ${column.escapedColumnName} @@ -234,8 +256,11 @@ #{item.scriptId,jdbcType=VARCHAR} + + #{item.name,jdbcType=VARCHAR} + - #{item.script,jdbcType=LONGVARCHAR} + #{item.script,jdbcType=LONGVARBINARY} ) diff --git a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql index 088d24abda..8a696a56f3 100644 --- a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql +++ b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql @@ -157,11 +157,12 @@ CREATE TABLE IF NOT EXISTS plugin DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '插件'; -CREATE TABLE IF NOT EXISTS plugin_front_script +CREATE TABLE IF NOT EXISTS plugin_script ( `plugin_id` VARCHAR(50) NOT NULL COMMENT '插件的ID' , `script_id` VARCHAR(50) NOT NULL COMMENT '插件中对应表单配置的ID' , - `script` TEXT COMMENT '脚本内容' , + `name` VARCHAR(255) COMMENT '插件中对应表单配置的名称' , + `script` LONGBLOB COMMENT '脚本内容' , PRIMARY KEY (plugin_id,script_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 diff --git a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java index dd91f94789..9675aa8061 100644 --- a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java +++ b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java @@ -4,10 +4,6 @@ import io.metersphere.plugin.sdk.api.AbstractMsPlugin; public abstract class AbstractApiPlugin extends AbstractMsPlugin { private static final String API_PLUGIN_TYPE = "API"; - public AbstractApiPlugin(ClassLoader pluginClassLoader) { - super(pluginClassLoader); - } - @Override public String getType() { return API_PLUGIN_TYPE; diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java index 9fb815786a..dcead1c8d8 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java @@ -4,10 +4,6 @@ import io.metersphere.plugin.sdk.api.AbstractMsPlugin; public abstract class AbstractPlatformPlugin extends AbstractMsPlugin { private static final String PLATFORM_PLUGIN_TYPE = "PLATFORM"; - public AbstractPlatformPlugin(ClassLoader pluginClassLoader) { - super(pluginClassLoader); - } - @Override public String getType() { return PLATFORM_PLUGIN_TYPE; diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java index 2bc3d1e629..73c5b021b5 100644 --- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java +++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java @@ -1,75 +1,16 @@ package io.metersphere.plugin.sdk.api; -import io.metersphere.plugin.sdk.util.PluginLogUtils; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - public abstract class AbstractMsPlugin implements MsPlugin { private static final String SCRIPT_DIR = "script"; - private ClassLoader pluginClassLoader; - - public AbstractMsPlugin(ClassLoader pluginClassLoader) { - this.pluginClassLoader = pluginClassLoader; - } - - protected InputStream readResource(String name) { - return pluginClassLoader.getResourceAsStream(name); - } - /** - * @return 返回该加载前端配置文件的目录,默认是 script + * @return 返回默认的前端配置文件的目录 * 可以重写定制 */ - protected String getScriptDir() { - return SCRIPT_DIR; - } - - /** - * @return 返回 resources下的 script 下的 json 文件 - */ @Override - public List getFrontendScript() { - List scriptList = new ArrayList<>(); - String scriptDirName = getScriptDir(); - URL scriptDir = pluginClassLoader.getResource(scriptDirName); - if (scriptDir != null) { - File resourceDir = new File(scriptDir.getFile()); - List filePaths = getFilePaths(resourceDir); - for (String filePath : filePaths) { - InputStream in = readResource(scriptDirName + "/" + filePath); - try { - if (in != null) { - scriptList.add(IOUtils.toString(in)); - } - } catch (IOException e) { - PluginLogUtils.error(e); - } - } - } - return scriptList; - } - - private static List getFilePaths(File directory) { - List filePaths = new ArrayList<>(); - File[] files = directory.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - filePaths.addAll(getFilePaths(file)); - } else { - filePaths.add(file.getAbsolutePath()); - } - } - } - return filePaths; + public String getScriptDir() { + return SCRIPT_DIR; } @Override @@ -79,6 +20,6 @@ public abstract class AbstractMsPlugin implements MsPlugin { @Override public String getPluginId() { - return getName().toLowerCase() + "-" + getVersion(); + return getKey().toLowerCase() + "-" + getVersion(); } } diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java index 2f815f138a..623ab6b834 100644 --- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java +++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java @@ -1,7 +1,5 @@ package io.metersphere.plugin.sdk.api; -import java.util.List; - /** * 插件的基本信息 * @@ -37,14 +35,14 @@ public interface MsPlugin { */ String getPluginId(); - /** - * @return 返回前端渲染需要的数据 - * 默认会返回 resources下的 script 下的 json 文件 - */ - List getFrontendScript(); - /** * @return 返回插件的版本 */ String getVersion(); + + /** + * @return 返回该加载前端配置文件的目录,默认是 script + * 可以重写定制 + */ + String getScriptDir(); } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java index f213aca5ea..ee645145c0 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java @@ -20,6 +20,8 @@ public class SystemInterceptor { configList.add(new MybatisInterceptorConfig(NoviceStatistics.class, "dataOption", CompressUtils.class, "zip", "unzip")); configList.add(new MybatisInterceptorConfig(ServiceIntegration.class, "configuration", CompressUtils.class, "zip", "unzip")); configList.add(new MybatisInterceptorConfig(UserExtend.class, "platformInfo", CompressUtils.class, "zip", "unzip")); + configList.add(new MybatisInterceptorConfig(PluginScript.class, "script", CompressUtils.class, "zip", "unzip")); + configList.add(new MybatisInterceptorConfig(PluginScript.class, "script", CompressUtils.class, "zip", "unzip")); return configList; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java new file mode 100644 index 0000000000..6cc787ab87 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java @@ -0,0 +1,6 @@ +package io.metersphere.sdk.constants; + +public class KafkaPluginTopicType { + public static final String ADD = "ADD"; + public static final String DELETE = "DELETE"; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java new file mode 100644 index 0000000000..5f170654ce --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java @@ -0,0 +1,5 @@ +package io.metersphere.sdk.constants; + +public class KafkaTopicConstants { + public static final String PLUGIN = "PLUGIN"; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java index 5f5b96ec66..a29c33e86c 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java @@ -1,5 +1,5 @@ package io.metersphere.sdk.constants; public enum PluginScenarioType { - PAI, PLATFORM + API, PLATFORM } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java deleted file mode 100644 index f455173175..0000000000 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.metersphere.sdk.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.io.Serializable; - -@Data -public class OrganizationOptionDTO implements Serializable { - private static final long serialVersionUID = 1L; - - @Schema(title = "组织ID") - private String id; - - @Schema(title = "组织编号") - private Long num; - - @Schema(title = "组织名称") - private String name; -} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java index 9e8b8ea7a1..a851176682 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java @@ -11,6 +11,10 @@ public class MSException extends RuntimeException { super(message); } + public MSException(Throwable t) { + super(t); + } + public MSException(IResultCode errorCode) { super(StringUtils.EMPTY); this.errorCode = errorCode; @@ -26,6 +30,10 @@ public class MSException extends RuntimeException { this.errorCode = errorCode; } + public MSException(String message, Throwable t) { + super(message, t); + } + public IResultCode getErrorCode() { return errorCode; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java index 40bdcd8e57..d354accf7c 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java @@ -2,6 +2,9 @@ package io.metersphere.sdk.file; import org.springframework.web.multipart.MultipartFile; +import java.io.InputStream; +import java.util.List; + public interface FileRepository { /** * 保存文件 @@ -11,7 +14,7 @@ public interface FileRepository { * @return * @throws Exception */ - public String saveFile(MultipartFile file, FileRequest request) throws Exception; + String saveFile(MultipartFile file, FileRequest request) throws Exception; /** * 保存文件 @@ -21,7 +24,7 @@ public interface FileRepository { * @return * @throws Exception */ - public String saveFile(byte[] bytes, FileRequest request) throws Exception; + String saveFile(byte[] bytes, FileRequest request) throws Exception; /** * 删除文件 @@ -29,7 +32,7 @@ public interface FileRepository { * @param request * @throws Exception */ - public void delete(FileRequest request) throws Exception; + void delete(FileRequest request) throws Exception; /** * 删除文件夹 @@ -37,7 +40,7 @@ public interface FileRepository { * @param request * @throws Exception */ - public void deleteFolder(FileRequest request) throws Exception; + void deleteFolder(FileRequest request) throws Exception; /** @@ -47,7 +50,16 @@ public interface FileRepository { * @return * @throws Exception */ - public byte[] getFile(FileRequest request) throws Exception; + byte[] getFile(FileRequest request) throws Exception; + + /** + * 获取文件字输入流 + * + * @param request + * @return + * @throws Exception + */ + InputStream getFileAsStream(FileRequest request) throws Exception; /** * 流式处理方式,通过逐块地下载文件 @@ -56,6 +68,14 @@ public interface FileRepository { * @param localPath * @throws Exception */ - public void downloadFile(FileRequest request, String localPath) throws Exception; + void downloadFile(FileRequest request, String localPath) throws Exception; + + /** + * 获取指定文件夹下的文件名列表 + * + * @param request + * @throws Exception + */ + List getFolderFileNames(FileRequest request) throws Exception; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java index 4568ce9667..9c45ca4bd6 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java @@ -24,8 +24,8 @@ public class MinioRepository implements FileRepository { // 文件存储路径 return StringUtils.join( request.getProjectId(), - File.separator, - StringUtils.isNotBlank(request.getResourceId()) ? request.getResourceId() + File.separator : StringUtils.EMPTY, + "/", + StringUtils.isNotBlank(request.getResourceId()) ? request.getResourceId() + "/" : StringUtils.EMPTY, request.getFileName()); } @@ -68,6 +68,11 @@ public class MinioRepository implements FileRepository { removeObjects(MinioConfig.BUCKET, filePath); } + @Override + public List getFolderFileNames(FileRequest request) throws Exception { + return listObjects(MinioConfig.BUCKET, getPath(request)); + } + private boolean removeObject(String bucketName, String objectName) throws Exception { client.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) // 存储桶 @@ -128,6 +133,7 @@ public class MinioRepository implements FileRepository { } } + @Override public InputStream getFileAsStream(FileRequest request) throws Exception { String fileName = getPath(request); return client.getObject(GetObjectArgs.builder() diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java index 515c350dda..5404922ae8 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java @@ -49,7 +49,7 @@ public class OperationLogModule { public static final String UI_AUTOMATION = "UI_AUTOMATION"; public static final String UI_AUTOMATION_REPORT = "UI_AUTOMATION_REPORT"; public static final String UI_AUTOMATION_SCHEDULE = "UI_AUTOMATION_SCHEDULE"; - public static final String PLUGIN_MANAGE = "PLUGIN_MANAGE"; + public static final String SYSTEM_PLUGIN = "SYSTEM_PLUGIN"; public static final String SYSTEM_PROJECT = "SYSTEM_PROJECT"; public static final String SYSTEM_PROJECT_MEMBER = "SYSTEM_PROJECT_MEMBER"; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java index 5fe152cc36..dad09a64f3 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java @@ -1,6 +1,7 @@ package io.metersphere.sdk.plugin.loader; import io.metersphere.sdk.plugin.storage.StorageStrategy; +import io.metersphere.sdk.util.LogUtils; import org.apache.commons.lang3.StringUtils; import java.io.*; @@ -25,12 +26,12 @@ public class PluginClassLoader extends ClassLoader { /** * 保存加载失败的类,之后重试 */ - protected Map loadErrorMap = new HashMap<>(); + protected Map loadErrorMap = new HashMap<>(); - private class byteArrayWrapper { + private class ByteArrayWrapper { private byte[] values; - public byteArrayWrapper(byte[] values) { + public ByteArrayWrapper(byte[] values) { this.values = values; } @@ -49,14 +50,21 @@ public class PluginClassLoader extends ClassLoader { */ protected StorageStrategy storageStrategy; + protected boolean isNeedUploadFile; + public PluginClassLoader() { // 将父加载器设置成当前的类加载器,目的是由父加载器加载接口,实现类由该加载器加载 super(PluginClassLoader.class.getClassLoader()); } public PluginClassLoader(StorageStrategy storageStrategy) { + this(storageStrategy, true); + } + + public PluginClassLoader(StorageStrategy storageStrategy, boolean isNeedUploadFile) { this(); this.storageStrategy = storageStrategy; + this.isNeedUploadFile = isNeedUploadFile; } public StorageStrategy getStorageStrategy() { @@ -106,7 +114,7 @@ public class PluginClassLoader extends ClassLoader { * * @param in */ - public void loadJar(InputStream in) throws IOException { + public void loadJar(InputStream in) throws Exception { if (in != null) { try (JarInputStream jis = new JarInputStream(in)) { JarEntry je; @@ -132,8 +140,8 @@ public class PluginClassLoader extends ClassLoader { JarEntry je = en.nextElement(); try (InputStream in = jar.getInputStream(je)) { loadJar(in, je); - } catch (IOException e) { -// LogUtils.error(e); + } catch (Exception e) { + LogUtils.error(e); } } reloadErrorClazz(); @@ -146,7 +154,7 @@ public class PluginClassLoader extends ClassLoader { * @param je * @throws IOException */ - protected void loadJar(InputStream in, JarEntry je) throws IOException { + protected void loadJar(InputStream in, JarEntry je) throws Exception { je.getName(); String name = je.getName(); if (name.endsWith(".class")) { @@ -167,13 +175,13 @@ public class PluginClassLoader extends ClassLoader { Class clazz = defineClass(className, bytes, 0, bytes.length); clazzSet.add(clazz); } catch (NoClassDefFoundError e) { - loadErrorMap.put(className, new byteArrayWrapper(bytes)); + loadErrorMap.put(className, new ByteArrayWrapper(bytes)); } catch (Throwable e) { -// LogUtils.error(e); + LogUtils.error(e); } } else if (!name.endsWith("/")) { // 非目录即静态资源 - if (storageStrategy != null) { + if (storageStrategy != null && isNeedUploadFile) { storageStrategy.store(name, in); } } @@ -190,13 +198,13 @@ public class PluginClassLoader extends ClassLoader { while (iterator.hasNext()) { String className = iterator.next(); try { -// LogUtils.info("reload class: " + className); + LogUtils.info("reload class: " + className); byte[] bytes = loadErrorMap.get(className).getValues(); Class clazz = defineClass(className, bytes, 0, bytes.length); clazzSet.add(clazz); iterator.remove(); } catch (Throwable e) { -// LogUtils.error(e); + LogUtils.error(e); } } } @@ -212,8 +220,8 @@ public class PluginClassLoader extends ClassLoader { if (null != storageStrategy) { try { return storageStrategy.get(name); - } catch (IOException e) { -// LogUtils.error(e, logger); + } catch (Exception e) { + LogUtils.error(e); return null; } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java index f48c922ba7..7bd388c59b 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java @@ -55,15 +55,15 @@ public class PluginManager { * * @param in */ - public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy) throws IOException { - PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy); + public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy, boolean isNeedUploadFile) throws Exception { + PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy, isNeedUploadFile); classLoaderMap.put(pluginId, pluginClassLoader); pluginClassLoader.loadJar(in); return this; } - public PluginManager loadJar(String pluginId, InputStream in) throws IOException { - return this.loadJar(pluginId, in, null); + public PluginManager loadJar(String pluginId, InputStream in, boolean isNeedUploadFile) throws Exception { + return this.loadJar(pluginId, in, null, isNeedUploadFile); } /** @@ -87,6 +87,9 @@ public class PluginManager { public T getImplInstance(String pluginId, Class superClazz) { try { Class clazz = getImplClass(pluginId, superClazz); + if (clazz == null) { + throw new MSException("未找到插件实现类"); + } return clazz.getConstructor().newInstance(); } catch (InvocationTargetException e) { LogUtils.error(e); diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java deleted file mode 100644 index 0af2304682..0000000000 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.metersphere.sdk.plugin.storage; - -import java.io.IOException; -import java.io.InputStream; - -/** - * jar包静态资源存储策略,存储在 Minio - */ -public class MinioStorageStrategy implements StorageStrategy { - - private String pluginId; - - public static final String DIR_PATH = "system/plugin"; - - public MinioStorageStrategy(String pluginId) { - this.pluginId = pluginId; - } - - @Override - public String store(String name, InputStream in) throws IOException { - // todo 上传到 minio - return null; - } - - @Override - public InputStream get(String name) { - // todo 获取文件 - return null; - } - - @Override - public void delete() throws IOException { - // todo 删除文件 - } -} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java new file mode 100644 index 0000000000..959b949afb --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java @@ -0,0 +1,64 @@ +package io.metersphere.sdk.plugin.storage; + +import io.metersphere.sdk.file.FileCenter; +import io.metersphere.sdk.file.FileRepository; +import io.metersphere.sdk.file.FileRequest; +import org.apache.commons.lang3.StringUtils; + +import java.io.InputStream; +import java.util.List; + +/** + * jar包静态资源存储策略 + * @author jianxing + */ +public class MsStorageStrategy implements StorageStrategy { + + + private final FileRepository fileRepository; + private final String pluginId; + + public static final String DIR_PATH = "system/plugin"; + + public MsStorageStrategy(String pluginId) { + this.pluginId = pluginId; + fileRepository = FileCenter.getDefaultRepository(); + } + + @Override + public String store(String name, InputStream in) throws Exception { + FileRequest request = getFileRequest(name); + return fileRepository.saveFile(in.readAllBytes(), request); + } + + @Override + public InputStream get(String name) throws Exception { + FileRequest request = getFileRequest(name); + return fileRepository.getFileAsStream(request); + } + + @Override + public List getFolderFileNames(String dirName) throws Exception { + FileRequest request = getFileRequest(dirName); + List fileNames = fileRepository.getFolderFileNames(request); + return fileNames.stream().map(s -> s.replace(getPluginDir(), StringUtils.EMPTY)).toList(); + } + + @Override + public void delete() throws Exception { + FileRequest request = new FileRequest(); + request.setProjectId(getPluginDir()); + fileRepository.deleteFolder(request); + } + + private FileRequest getFileRequest(String name) { + FileRequest request = new FileRequest(); + request.setProjectId(getPluginDir()); + request.setFileName(name); + return request; + } + + private String getPluginDir() { + return DIR_PATH + "/" + this.pluginId; + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java index c5794566ab..5ce880f51f 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java @@ -2,9 +2,11 @@ package io.metersphere.sdk.plugin.storage; import java.io.IOException; import java.io.InputStream; +import java.util.List; /** * jar包、图片、前端配置文件等静态资源存储策略 + * @author jianxing */ public interface StorageStrategy { @@ -15,7 +17,7 @@ public interface StorageStrategy { * @return * @throws IOException */ - String store(String name, InputStream in) throws IOException; + String store(String name, InputStream in) throws Exception; /** * 获取文件 @@ -23,12 +25,19 @@ public interface StorageStrategy { * @return * @throws IOException */ - InputStream get(String path) throws IOException; + InputStream get(String path) throws Exception; + /** + * 获取指定文件夹下的文件名列表 + * + * @param dirName + * @throws Exception + */ + List getFolderFileNames(String dirName) throws Exception; /** * 删除文件 * @throws IOException */ - void delete() throws IOException; + void delete() throws Exception; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java new file mode 100644 index 0000000000..4ddd2cb06c --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java @@ -0,0 +1,190 @@ +package io.metersphere.sdk.service; + +import io.metersphere.plugin.sdk.api.MsPlugin; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.plugin.loader.PluginClassLoader; +import io.metersphere.sdk.plugin.loader.PluginManager; +import io.metersphere.sdk.plugin.storage.MsStorageStrategy; +import io.metersphere.sdk.plugin.storage.StorageStrategy; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.system.domain.Plugin; +import io.metersphere.system.domain.PluginExample; +import io.metersphere.system.mapper.PluginMapper; +import jakarta.annotation.Resource; +import org.codehaus.plexus.util.IOUtil; +import org.codehaus.plexus.util.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @author jianxing + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class PluginLoadService { + + private final PluginManager pluginManager = new PluginManager(); + + @Resource + private PluginMapper pluginMapper; + + /** + * 上传插件到 minio + */ + public void uploadPlugin(String id, MultipartFile file) { + try { + getStorageStrategy(id).store(file.getOriginalFilename(), file.getInputStream()); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException("文件上传异常", e); + } + } + + /** + * @return 返回前端渲染需要的数据 + * 默认会返回 resources下的 script 下的 json 文件 + */ + public List getFrontendScripts(String pluginId) { + MsPlugin msPluginInstance = getMsPluginInstance(pluginId); + String scriptDir = msPluginInstance.getScriptDir(); + StorageStrategy storageStrategy = pluginManager.getClassLoader(pluginId).getStorageStrategy(); + try { + // 查询脚本文件名 + List folderFileNames = storageStrategy.getFolderFileNames(scriptDir); + // 获取脚本内容 + List scripts = new ArrayList<>(folderFileNames.size()); + for (String folderFileName : folderFileNames) { + InputStream in = storageStrategy.get(folderFileName); + if (in == null) { + continue; + } + scripts.add(IOUtil.toString(storageStrategy.get(folderFileName))); + } + return scripts; + } catch (Exception e) { + LogUtils.error(e); + throw new MSException("获取脚本异常", e); + } + } + + private static StorageStrategy getStorageStrategy(String id) { + return new MsStorageStrategy(id); + } + + public void loadPlugin(String id, MultipartFile file) { + // 加载 jar + InputStream inputStream; + try { + inputStream = file.getInputStream(); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException("获取文件输入流异常", e); + } + loadPlugin(id, inputStream, true); + } + + public void loadPlugin(String pluginId, String fileName) { + PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId); + if (classLoader != null) { + return; + } + // 加载 jar + InputStream inputStream; + try { + inputStream = classLoader.getStorageStrategy().get(fileName); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException("下载文件异常", e); + } + loadPlugin(pluginId, inputStream, false); + } + + public void loadPlugin(String id, InputStream inputStream, boolean isNeedUploadFile) { + if (inputStream == null) { + return; + } + loadPlugin(id, inputStream, new MsStorageStrategy(id), isNeedUploadFile); + } + + /** + * 加载插件 + * + * @param id 插件ID + * @param inputStream 输入流 + * @param storageStrategy 静态文件及jar包存储策略 + */ + public void loadPlugin(String id, InputStream inputStream, StorageStrategy storageStrategy, boolean isNeedUploadFile) { + if (inputStream == null || pluginManager.getClassLoader(id) != null) { + return; + } + // 加载 jar + try { + pluginManager.loadJar(id, inputStream, + storageStrategy == null ? getStorageStrategy(id) : storageStrategy, isNeedUploadFile); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException("加载插件异常", e); + } + } + + /** + * 项目启动时加载插件 + */ + public synchronized void loadPlugins() { + List plugins = pluginMapper.selectByExample(new PluginExample()); + plugins.forEach(plugin -> { + String id = plugin.getId(); + StorageStrategy storageStrategy = getStorageStrategy(id); + try { + InputStream inputStream = storageStrategy.get(plugin.getFileName()); + loadPlugin(id, inputStream, storageStrategy, false); + } catch (Exception e) { + LogUtils.error("初始化插件异常" + plugin.getFileName(), e); + } + }); + } + + /** + * 卸载插件 + */ + public void unloadPlugin(String pluginId) { + pluginManager.deletePlugin(pluginId); + } + + /** + * 删除插件 + */ + public void deletePlugin(String pluginId) { + // 删除文件 + PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId); + try { + if (classLoader != null) { + classLoader.getStorageStrategy().delete(); + } + } catch (Exception e) { + LogUtils.error(e); + throw new MSException("删除插件异常 ", e); + } + unloadPlugin(pluginId); + } + + public MsPlugin getMsPluginInstance(String id) { + return pluginManager.getImplInstance(id, MsPlugin.class); + } + + public boolean hasPluginKey(String currentPluginId, String pluginKey) { + for (String pluginId : pluginManager.getClassLoaderMap().keySet()) { + MsPlugin msPlugin = getMsPluginInstance(pluginId); + if (!StringUtils.equals(currentPluginId, pluginId) && StringUtils.equals(msPlugin.getKey(), pluginKey)) { + return true; + } + } + return false; + } +} diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index 850cf551aa..bbc4393b1f 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -168,7 +168,10 @@ permission.system_plugin.read=READ permission.system_plugin.add=CREATE permission.system_plugin.edit=UPDATE permission.system_plugin.delete=DELETE - +plugin.exist=plugin name or filename already exists +plugin.type.exist=plugin type already exists +plugin.script.exist=duplicate script id +plugin.script.format=malformed script diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index 23c3354db8..778f0a1231 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -166,4 +166,8 @@ permission.system_plugin.name=插件 permission.system_plugin.read=查看插件 permission.system_plugin.add=创建插件 permission.system_plugin.edit=更新插件 -permission.system_plugin.delete=删除插件 \ No newline at end of file +permission.system_plugin.delete=删除插件 +plugin.exist=插件名称或文件名已存在 +plugin.type.exist=插件类型已存在 +plugin.script.exist=脚本id重复 +plugin.script.format=脚本格式错误 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index 84352711ce..8c480cfd83 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -167,4 +167,8 @@ permission.system_plugin.read=查看插件 permission.system_plugin.add=創建插件 permission.system_plugin.edit=更新插件 permission.system_plugin.delete=刪除插件 +plugin.exist=插件名稱或文件名已存在 +plugin.type.exist=插件類型已存在 +plugin.script.exist=腳本id重複 +plugin.script.format=腳本格式錯誤 diff --git a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java index 3c470c1620..e081484c5a 100644 --- a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java +++ b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java @@ -163,6 +163,12 @@ public abstract class BaseTest { .andExpect(status().isOk()); } + protected ResultActions requestMultipart(String url, MultiValueMap paramMap, Object... uriVariables) throws Exception { + MockHttpServletRequestBuilder requestBuilder = getMultipartRequestBuilder(url, paramMap, uriVariables); + return mockMvc.perform(requestBuilder) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + protected MvcResult requestMultipartWithOkAndReturn(String url, MultiValueMap paramMap, Object... uriVariables) throws Exception { return this.requestMultipartWithOk(url, paramMap, uriVariables).andReturn(); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java index 3ae0397a52..2a44ca6cef 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java @@ -8,6 +8,7 @@ import io.metersphere.sdk.util.SessionUtils; import io.metersphere.system.domain.Plugin; import io.metersphere.system.dto.PluginDTO; import io.metersphere.system.request.PluginUpdateRequest; +import io.metersphere.system.service.PluginLogService; import io.metersphere.system.service.PluginService; import io.metersphere.validation.groups.Created; import io.metersphere.validation.groups.Updated; @@ -40,17 +41,10 @@ public class PluginController { return pluginService.list(); } - @GetMapping("/get/{id}") - @Operation(summary = "获取插件详情") - @RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_READ) - public PluginDTO get(@PathVariable String id) { - return pluginService.get(id); - } - @PostMapping("/add") @Operation(summary = "创建插件") @RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_ADD) - @Log(type = OperationLogType.UPDATE, expression = "#msClass.addLog(#request)", msClass = PluginService.class) + @Log(type = OperationLogType.UPDATE, expression = "#msClass.addLog(#request)", msClass = PluginLogService.class) public Plugin add(@Validated({Created.class}) @RequestPart(value = "request") PluginUpdateRequest request, @RequestPart(value = "file") MultipartFile file) { request.setCreateUser(SessionUtils.getUserId()); @@ -60,26 +54,25 @@ public class PluginController { @PostMapping("/update") @Operation(summary = "更新插件") @RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_UPDATE) - @Log(type = OperationLogType.ADD, expression = "#msClass.updateLog(#request)", msClass = PluginService.class) - public Plugin update(@Validated({Updated.class}) @RequestPart(value = "request") PluginUpdateRequest request, - @RequestPart(value = "file", required = false) MultipartFile file) { + @Log(type = OperationLogType.ADD, expression = "#msClass.updateLog(#request)", msClass = PluginLogService.class) + public Plugin update(@Validated({Updated.class}) @RequestBody PluginUpdateRequest request) { Plugin plugin = new Plugin(); BeanUtils.copyBean(plugin, request); - return pluginService.update(request, file); + return pluginService.update(request); } @GetMapping("/delete/{id}") @Operation(summary = "删除插件") @RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_DELETE) - @Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = PluginService.class) - public String delete(@PathVariable String id) { - return pluginService.delete(id); + @Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = PluginLogService.class) + public void delete(@PathVariable String id) { + pluginService.delete(id); } @GetMapping("/script/get/{pluginId}/{scriptId}") @Operation(summary = "获取插件对应表单的脚本内容") @RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_READ) - public String getScript(@PathVariable String pluginId, String scriptId) { + public String getScript(@PathVariable String pluginId, @PathVariable String scriptId) { return pluginService.getScript(pluginId, scriptId); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java index 8fa0f5eb8d..5984e57ae7 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java @@ -16,7 +16,11 @@ public enum SystemResultCode implements IResultCode { /** * 获取/编辑组织自定义用户组,如果非组织自定义用户组,会返回该响应码 */ - NO_ORG_USER_ROLE_PERMISSION(101007, "organization_user_role_permission_error"); + NO_ORG_USER_ROLE_PERMISSION(101007, "organization_user_role_permission_error"), + PLUGIN_EXIST(101008, "plugin.exist"), + PLUGIN_TYPE_EXIST(101009, "plugin.type.exist"), + PLUGIN_SCRIPT_EXIST(101010, "plugin.script.exist"), + PLUGIN_SCRIPT_FORMAT(101011, "plugin.script.format"); private final int code; private final String message; diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java index 2d09828c05..dac3490618 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java @@ -1,6 +1,6 @@ package io.metersphere.system.dto; -import io.metersphere.sdk.dto.OrganizationOptionDTO; +import io.metersphere.sdk.dto.OptionDTO; import io.metersphere.system.domain.Plugin; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -15,14 +15,8 @@ public class PluginDTO extends Plugin implements Serializable { private static final long serialVersionUID = 1L; @Schema(title = "插件前端表单配置项列表") - private List pluginForms; + private List pluginForms; @Schema(title = "关联的组织列表") - private List organizations; - - @Data - class PluginForm { - private String id; - private String name; - } + private List organizations; } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java b/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java new file mode 100644 index 0000000000..39aa408f0a --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java @@ -0,0 +1,40 @@ +package io.metersphere.system.listener; + + +import io.metersphere.sdk.constants.KafkaPluginTopicType; +import io.metersphere.sdk.constants.KafkaTopicConstants; +import io.metersphere.sdk.service.PluginLoadService; +import io.metersphere.sdk.util.LogUtils; +import jakarta.annotation.Resource; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class PluginListener { + + public static final String PLUGIN_CONSUMER = "plugin_consumer"; + + @Resource + private PluginLoadService pluginLoadService; + + // groupId 必须是每个实例唯一 + @KafkaListener(id = PLUGIN_CONSUMER, topics = KafkaTopicConstants.PLUGIN, groupId = PLUGIN_CONSUMER + "_" + "${random.uuid}") + public void handlePluginChange(ConsumerRecord record) { + LogUtils.info("Service consume platform_plugin message: " + record); + String[] info = record.value().split(":"); + String operate = info[0]; + String pluginId = info[1]; + switch (operate) { + case KafkaPluginTopicType.ADD: + String pluginName = info[2]; + pluginLoadService.loadPlugin(pluginId, pluginName); + break; + case KafkaPluginTopicType.DELETE: + pluginLoadService.unloadPlugin(pluginId); + break; + default: + break; + } + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java index ea69f6326f..333ed2638d 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java @@ -1,5 +1,6 @@ package io.metersphere.system.mapper; +import io.metersphere.sdk.dto.OptionDTO; import io.metersphere.system.domain.User; import io.metersphere.system.dto.OrgUserExtend; import io.metersphere.system.dto.OrganizationDTO; @@ -75,4 +76,6 @@ public interface ExtOrganizationMapper { * @return 组织列表数据 */ List selectOrganizationOptions(); + + List getOptionsByIds(@Param("ids") List ids); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml index a0f440daa9..47eea4af03 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml @@ -77,6 +77,12 @@ + diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java new file mode 100644 index 0000000000..1a03b9e565 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java @@ -0,0 +1,9 @@ +package io.metersphere.system.mapper; + +import io.metersphere.system.dto.PluginDTO; + +import java.util.List; + +public interface ExtPluginMapper { + List getPlugins(); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml new file mode 100644 index 0000000000..4a1bb3b2a4 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java new file mode 100644 index 0000000000..f3816dc9ca --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java @@ -0,0 +1,10 @@ +package io.metersphere.system.mapper; + +import io.metersphere.system.domain.PluginScript; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ExtPluginScriptMapper { + List getOptionByPluginIds(@Param("pluginIds") List pluginIds); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml new file mode 100644 index 0000000000..3fa43737a4 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java index 55f2538a59..e1442113bf 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java @@ -13,7 +13,7 @@ import java.util.List; public class PluginUpdateRequest { @Schema(title = "ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "{plugin.id.not_blank}", groups = {Updated.class}) - @Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Created.class, Updated.class}) + @Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Updated.class}) private String id; @Schema(title = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java index 18bedb7999..05e8d176dc 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java @@ -25,6 +25,7 @@ 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.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -62,6 +63,9 @@ public class OrganizationService { private SystemProjectService systemProjectService; @Resource private UserRolePermissionMapper userRolePermissionMapper; + @Resource + @Lazy + private PluginOrganizationService pluginOrganizationService; private static final String ADD_MEMBER_PATH = "/system/organization/add-member"; private static final String REMOVE_MEMBER_PATH = "/system/organization/remove-member"; @@ -119,6 +123,9 @@ public class OrganizationService { userRolePermissionMapper.deleteByExample(userRolePermissionExample); } + // 删除组织和插件的关联关系 + pluginOrganizationService.deleteByOrgId(organizationId); + // TODO: 删除环境组, 删除定时任务 // 删除组织 organizationMapper.deleteByPrimaryKey(organizationId); @@ -723,4 +730,11 @@ public class OrganizationService { dto.setModifiedValue(JSON.toJSONBytes(modifiedValue)); logs.add(dto); } + + public List getOptionsByIds(List orgIds) { + if (CollectionUtils.isEmpty(orgIds)) { + return new ArrayList<>(0); + } + return extOrganizationMapper.getOptionsByIds(orgIds); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java new file mode 100644 index 0000000000..1049ea7be5 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java @@ -0,0 +1,68 @@ +package io.metersphere.system.service; + +import io.metersphere.system.domain.Plugin; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import io.metersphere.sdk.constants.OperationLogConstants; +import io.metersphere.sdk.log.constants.OperationLogModule; +import io.metersphere.sdk.log.constants.OperationLogType; +import io.metersphere.sdk.dto.LogDTO; +import io.metersphere.sdk.util.JSON; +import org.springframework.transaction.annotation.Transactional; +import io.metersphere.system.request.PluginUpdateRequest; + +/** + * @author jianxing + * @date : 2023-8-3 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class PluginLogService { + + @Resource + private PluginService pluginService; + + public LogDTO addLog(PluginUpdateRequest request) { + LogDTO dto = new LogDTO( + OperationLogConstants.SYSTEM, + OperationLogConstants.SYSTEM, + null, + null, + OperationLogType.ADD.name(), + OperationLogModule.SYSTEM_PLUGIN, + request.getName()); + dto.setOriginalValue(JSON.toJSONBytes(request)); + return dto; + } + + public LogDTO updateLog(PluginUpdateRequest request) { + Plugin plugin = pluginService.get(request.getId()); + LogDTO dto = new LogDTO( + OperationLogConstants.SYSTEM, + OperationLogConstants.SYSTEM, + plugin.getId(), + null, + OperationLogType.UPDATE.name(), + OperationLogModule.SYSTEM_PLUGIN, + plugin.getName()); + dto.setOriginalValue(JSON.toJSONBytes(plugin)); + return dto; + } + + public LogDTO deleteLog(String id) { + Plugin plugin = pluginService.get(id); + if (plugin == null) { + return null; + } + LogDTO dto = new LogDTO( + OperationLogConstants.SYSTEM, + OperationLogConstants.SYSTEM, + plugin.getId(), + null, + OperationLogType.DELETE.name(), + OperationLogModule.SYSTEM_PLUGIN, + plugin.getName()); + dto.setOriginalValue(JSON.toJSONBytes(plugin)); + return dto; + } +} \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java new file mode 100644 index 0000000000..979d147d6e --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java @@ -0,0 +1,89 @@ +package io.metersphere.system.service; + +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.system.domain.PluginOrganization; +import io.metersphere.system.domain.PluginOrganizationExample; +import io.metersphere.system.mapper.PluginOrganizationMapper; +import jakarta.annotation.Resource; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional(rollbackFor = Exception.class) +public class PluginOrganizationService { + + @Resource + private PluginOrganizationMapper pluginOrganizationMapper; + @Resource + private OrganizationService organizationService; + + public void add(String pluginId, List orgIds) { + if (CollectionUtils.isEmpty(orgIds)) { + return; + } + List pluginOrganizations = new ArrayList<>(orgIds.size()); + for (String orgId : orgIds) { + PluginOrganization pluginOrganization = new PluginOrganization(); + pluginOrganization.setPluginId(pluginId); + pluginOrganization.setOrganizationId(orgId); + pluginOrganizations.add(pluginOrganization); + } + pluginOrganizationMapper.batchInsert(pluginOrganizations); + } + + public void deleteByPluginId(String pluginId) { + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + pluginOrganizationMapper.deleteByExample(example); + } + + public void deleteByOrgId(String orgId) { + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andOrganizationIdEqualTo(orgId); + pluginOrganizationMapper.deleteByExample(example); + } + + public void update(String pluginId, List organizationIds) { + if (organizationIds == null) { + // 如果参数没填,则不更新 + return; + } + // 先删除关联关系 + deleteByPluginId(pluginId); + // 重新添加关联关系 + add(pluginId, organizationIds); + } + + public Map> getOrgMap(List pluginIds) { + if (CollectionUtils.isEmpty(pluginIds)) { + return Collections.emptyMap(); + } + // 查询插件和组织的关联关系 + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andPluginIdIn(pluginIds); + List pluginOrganizations = pluginOrganizationMapper.selectByExample(example); + + // 查询组织信息 + List orgIds = pluginOrganizations.stream().map(PluginOrganization::getOrganizationId).toList(); + List orgList = organizationService.getOptionsByIds(orgIds); + Map orgInfoMap = orgList.stream().collect(Collectors.toMap(OptionDTO::getId, i -> i)); + + // 组装成 map + Map> orgMap = new HashMap<>(); + for (PluginOrganization pluginOrganization : pluginOrganizations) { + String pluginId = pluginOrganization.getPluginId(); + String orgId = pluginOrganization.getOrganizationId(); + OptionDTO orgInfo = orgInfoMap.get(orgId); + if (orgInfo == null) { + continue; + } + orgMap.computeIfAbsent(pluginId, k -> new ArrayList<>()) + .add(orgInfo); + } + return orgMap; + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java new file mode 100644 index 0000000000..a49e1a029f --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java @@ -0,0 +1,84 @@ +package io.metersphere.system.service; + +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.domain.PluginScript; +import io.metersphere.system.domain.PluginScriptExample; +import io.metersphere.system.mapper.ExtPluginScriptMapper; +import io.metersphere.system.mapper.PluginScriptMapper; +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 java.util.*; + +import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_SCRIPT_EXIST; +import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_SCRIPT_FORMAT; + +@Service +@Transactional(rollbackFor = Exception.class) +public class PluginScriptService { + + @Resource + private PluginScriptMapper pluginScriptMapper; + @Resource + private ExtPluginScriptMapper extPluginScriptMapper; + + public void add(String pluginId, List frontendScript) { + Set ids = new HashSet<>(); + List pluginScripts = new ArrayList<>(frontendScript.size()); + for (String script : frontendScript) { + PluginScript pluginScript = new PluginScript(); + OptionDTO scriptInfo; + try { + scriptInfo = JSON.parseObject(script, OptionDTO.class); + } catch (Exception e) { + throw new MSException(PLUGIN_SCRIPT_FORMAT); + } + // ID 判重 + if (ids.contains(scriptInfo.getId())) { + throw new MSException(PLUGIN_SCRIPT_EXIST); + } + ids.add(scriptInfo.getId()); + pluginScript.setPluginId(pluginId); + pluginScript.setScriptId( + StringUtils.isBlank(scriptInfo.getId()) ? UUID.randomUUID().toString() : scriptInfo.getId() + ); + pluginScript.setName(scriptInfo.getName()); + pluginScript.setScript(script.getBytes()); + pluginScripts.add(pluginScript); + } + pluginScriptMapper.batchInsert(pluginScripts); + } + + public void deleteByPluginId(String pluginId) { + PluginScriptExample example = new PluginScriptExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + pluginScriptMapper.deleteByExample(example); + } + + public String get(String pluginId, String scriptId) { + PluginScript frontScript = pluginScriptMapper.selectByPrimaryKey(pluginId, scriptId); + return frontScript == null ? null : new String(frontScript.getScript()); + } + + public Map> getScripteMap(List pluginIds) { + if (CollectionUtils.isEmpty(pluginIds)) { + return Collections.emptyMap(); + } + List scripts = extPluginScriptMapper.getOptionByPluginIds(pluginIds); + Map> scriptMap = new HashMap<>(); + for (PluginScript script : scripts) { + List scriptList = scriptMap.computeIfAbsent(script.getPluginId(), k -> new ArrayList<>()); + OptionDTO optionDTO = new OptionDTO(); + optionDTO.setId(script.getScriptId()); + optionDTO.setName(script.getName()); + scriptList.add(optionDTO); + } + return scriptMap; + + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java index a7f9e9ba65..1b8a3b8902 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java @@ -1,23 +1,34 @@ package io.metersphere.system.service; -import io.metersphere.sdk.constants.PluginScenarioType; +import io.metersphere.plugin.sdk.api.MsPlugin; +import io.metersphere.sdk.constants.KafkaPluginTopicType; +import io.metersphere.sdk.constants.KafkaTopicConstants; +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.service.PluginLoadService; import io.metersphere.sdk.util.BeanUtils; import io.metersphere.system.domain.Plugin; -import io.metersphere.system.domain.PluginFrontScript; +import io.metersphere.system.domain.PluginExample; import io.metersphere.system.dto.PluginDTO; -import io.metersphere.system.mapper.PluginFrontScriptMapper; +import io.metersphere.system.mapper.ExtPluginMapper; import io.metersphere.system.mapper.PluginMapper; import io.metersphere.system.request.PluginUpdateRequest; import jakarta.annotation.Resource; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; +import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_EXIST; +import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_TYPE_EXIST; + /** * @author jianxing * @date : 2023-7-13 @@ -29,50 +40,157 @@ public class PluginService { @Resource private PluginMapper pluginMapper; @Resource - private PluginFrontScriptMapper pluginFrontScriptMapper; + private ExtPluginMapper extPluginMapper; + @Resource + private PluginScriptService pluginScriptService; + @Resource + private PluginOrganizationService pluginOrganizationService; + @Resource + private PluginLoadService pluginLoadService; + @Resource + private KafkaTemplate kafkaTemplate; public List list() { - return new ArrayList<>(); + List plugins = extPluginMapper.getPlugins(); + List pluginIds = plugins.stream().map(Plugin::getId).toList(); + Map> scripteMap = pluginScriptService.getScripteMap(pluginIds); + Map> orgMap = pluginOrganizationService.getOrgMap(pluginIds); + plugins.forEach(plugin -> { + plugin.setPluginForms(scripteMap.get(plugin.getId())); + plugin.setOrganizations(orgMap.get(plugin.getId())); + }); + return plugins; } - public PluginDTO get(String id) { - Plugin plugin = pluginMapper.selectByPrimaryKey(id); - PluginDTO pluginDTO = new PluginDTO(); - BeanUtils.copyBean(plugin, pluginDTO); - return pluginDTO; + public Plugin get(String id) { + return pluginMapper.selectByPrimaryKey(id); } public Plugin add(PluginUpdateRequest request, MultipartFile file) { + String id = UUID.randomUUID().toString(); Plugin plugin = new Plugin(); BeanUtils.copyBean(plugin, request); - plugin.setId(UUID.randomUUID().toString()); - plugin.setPluginId(UUID.randomUUID().toString()); - plugin.setFileName(file.getName()); + plugin.setId(id); + plugin.setFileName(file.getOriginalFilename()); plugin.setCreateTime(System.currentTimeMillis()); plugin.setUpdateTime(System.currentTimeMillis()); - plugin.setXpack(false); - plugin.setScenario(PluginScenarioType.PAI.name()); - pluginMapper.insert(plugin); + + // 校验重名 + checkPluginAddExist(plugin); + + try { + // 加载插件 + pluginLoadService.loadPlugin(id, file); + // 上传插件 + pluginLoadService.uploadPlugin(id, file); + // 获取插件前端配置脚本 + List frontendScript = pluginLoadService.getFrontendScripts(id); + + MsPlugin msPlugin = pluginLoadService.getMsPluginInstance(id); + plugin.setScenario(msPlugin.getType()); + plugin.setXpack(msPlugin.isXpack()); + plugin.setPluginId(msPlugin.getPluginId()); + + // 校验插件类型是否重复 + checkPluginKeyExist(id, msPlugin.getKey()); + + // 保存插件脚本 + pluginScriptService.add(id, frontendScript); + + // 保存插件和组织的关联关系 + if (!request.getGlobal()) { + pluginOrganizationService.add(id, request.getOrganizationIds()); + } + + pluginMapper.insert(plugin); + + // 通知其他节点加载插件 + notifiedPluginAdd(id, plugin.getFileName()); + } catch (Exception e) { + // 删除插件 + pluginLoadService.deletePlugin(id); + throw e; + } return plugin; } - public Plugin update(PluginUpdateRequest request, MultipartFile file) { + private void checkPluginKeyExist(String pluginId, String pluginKey) { + if (pluginLoadService.hasPluginKey(pluginId, pluginKey)) { + throw new MSException(PLUGIN_TYPE_EXIST); + } + } + + private void checkPluginAddExist(Plugin plugin) { + PluginExample example = new PluginExample(); + example.createCriteria() + .andNameEqualTo(plugin.getName()); + PluginExample.Criteria criteria = example.createCriteria() + .andFileNameEqualTo(plugin.getFileName()); + example.or(criteria); + if (CollectionUtils.isNotEmpty(pluginMapper.selectByExample(example))) { + throw new MSException(PLUGIN_EXIST); + } + } + + /** + * 通知其他节点加载插件 + * 这里需要传一下 fileName,事务未提交,查询不到文件名 + * @param pluginId + * @param fileName + */ + public void notifiedPluginAdd(String pluginId, String fileName) { + // 初始化项目默认节点 + kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s:%s", KafkaPluginTopicType.ADD, pluginId, fileName)); + } + + /** + * 通知其他节点卸载插件 + * @param pluginId + */ + public void notifiedPluginDelete(String pluginId) { + // 初始化项目默认节点 + kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s", KafkaPluginTopicType.DELETE, pluginId)); + } + + public Plugin update(PluginUpdateRequest request) { + request.setCreateUser(null); Plugin plugin = new Plugin(); BeanUtils.copyBean(plugin, request); plugin.setCreateTime(null); plugin.setUpdateTime(null); - plugin.setCreateUser(null); + // 校验重名 + checkPluginUpdateExist(plugin); pluginMapper.updateByPrimaryKeySelective(plugin); + if (request.getGlobal()) { + // 全局插件,删除和组织的关联关系 + request.setOrganizationIds(new ArrayList<>(0)); + } + pluginOrganizationService.update(plugin.getId(), request.getOrganizationIds()); return plugin; } - public String delete(String id) { + private void checkPluginUpdateExist(Plugin plugin) { + PluginExample example = new PluginExample(); + example.createCriteria() + .andIdNotEqualTo(plugin.getId()) + .andNameEqualTo(plugin.getName()); + if (CollectionUtils.isNotEmpty(pluginMapper.selectByExample(example))) { + throw new MSException(PLUGIN_EXIST); + } + } + + public void delete(String id) { pluginMapper.deleteByPrimaryKey(id); - return id; + // 删除插件脚本 + pluginScriptService.deleteByPluginId(id); + // 删除和组织的关联关系 + pluginOrganizationService.deleteByPluginId(id); + // 删除和卸载插件 + pluginLoadService.deletePlugin(id); + notifiedPluginDelete(id); } public String getScript(String pluginId, String scriptId) { - PluginFrontScript frontScript = pluginFrontScriptMapper.selectByPrimaryKey(pluginId, scriptId); - return frontScript == null ? null : frontScript.getScript(); + return pluginScriptService.get(pluginId, scriptId); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml b/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml index 088b64a1ca..fad011c7b6 100644 --- a/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml +++ b/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml @@ -78,7 +78,7 @@
-
+
diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java index 8d5844b572..ed83729c15 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java @@ -2,20 +2,31 @@ package io.metersphere.system.controller; import io.metersphere.sdk.base.BaseTest; import io.metersphere.sdk.constants.PermissionConstants; -import io.metersphere.system.domain.Plugin; +import io.metersphere.sdk.constants.PluginScenarioType; +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.sdk.log.constants.OperationLogType; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.controller.param.PluginUpdateRequestDefinition; +import io.metersphere.system.domain.*; +import io.metersphere.system.dto.OrganizationDTO; +import io.metersphere.system.dto.PluginDTO; import io.metersphere.system.mapper.PluginMapper; +import io.metersphere.system.mapper.PluginOrganizationMapper; +import io.metersphere.system.mapper.PluginScriptMapper; import io.metersphere.system.request.PluginUpdateRequest; +import io.metersphere.system.service.OrganizationService; import jakarta.annotation.Resource; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.*; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.MultiValueMap; import java.io.File; +import java.util.*; + +import static io.metersphere.system.controller.result.SystemResultCode.*; /** * @author jianxing @@ -29,99 +40,240 @@ public class PluginControllerTests extends BaseTest { private static final String SCRIPT_GET = "script/get/{0}/{1}"; @Resource private PluginMapper pluginMapper; + @Resource + private PluginOrganizationMapper pluginOrganizationMapper; + @Resource + private PluginScriptMapper pluginScriptMapper; + @Resource + private OrganizationService organizationService; private static Plugin addPlugin; + private static Plugin anotherAddPlugin; + @Override protected String getBasePath() { return BASE_PATH; } @Test - public void list() throws Exception { - // @@请求成功 - this.requestGetWithOk(DEFAULT_LIST) - .andReturn(); -// List pluginList = getResultDataArray(mvcResult, Plugin.class); - // todo 校验数据是否正确 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_LIST); + @Order(0) + public void listEmpty() throws Exception { + // @@没有数据是校验是否成功 + this.requestGetWithOkAndReturn(DEFAULT_LIST); } @Test - @Order(0) + @Order(1) public void add() throws Exception { // @@请求成功 PluginUpdateRequest request = new PluginUpdateRequest(); + OrganizationDTO org = organizationService.getDefault(); + File jarFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-mqtt-plugin-3.x.jar") + .getPath() + ); + File anotherJarFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-jira-plugin-3.x.jar") + .getPath() + ); + request.setName("test"); request.setDescription("test desc"); - MultiValueMap multiValueMap = getDefaultMultiPartParam(request, - new File("src/test/resources/application.properties")); + request.setGlobal(false); + request.setEnable(false); + request.setOrganizationIds(Arrays.asList(org.getId())); + MultiValueMap multiValueMap = getDefaultMultiPartParam(request, jarFile); + MvcResult mvcResult = this.requestMultipartWithOkAndReturn(DEFAULT_ADD, multiValueMap); + // 校验数据是否正确 Plugin resultData = getResultData(mvcResult, Plugin.class); Plugin plugin = pluginMapper.selectByPrimaryKey(resultData.getId()); + Assertions.assertEquals(plugin.getName(), request.getName()); + Assertions.assertEquals(plugin.getDescription(), request.getDescription()); + Assertions.assertEquals(plugin.getEnable(), request.getEnable()); + Assertions.assertEquals(plugin.getGlobal(), request.getGlobal()); + Assertions.assertEquals(plugin.getXpack(), false); + Assertions.assertEquals(plugin.getFileName(), jarFile.getName()); + Assertions.assertEquals(plugin.getScenario(), PluginScenarioType.API.name()); + Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId())); + Assertions.assertEquals(Arrays.asList("connect", "disconnect", "pub", "sub"), getScriptIdsByPlugId(plugin.getId())); + addPlugin = plugin; + + // @@重名校验异常 + // 校验插件名称重名 + assertErrorCode(this.requestMultipart(DEFAULT_ADD, + getDefaultMultiPartParam(request, anotherJarFile)), PLUGIN_EXIST); + + // 校验文件名重名 + request.setName("test1"); + assertErrorCode(this.requestMultipart(DEFAULT_ADD, + getDefaultMultiPartParam(request, jarFile)), PLUGIN_EXIST); + + // 校验插件 key 重复 + File typeRepeatFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-mqtt-plugin-repeat-key.jar") + .getPath() + ); + assertErrorCode(this.requestMultipart(DEFAULT_ADD, + getDefaultMultiPartParam(request, typeRepeatFile)), PLUGIN_TYPE_EXIST); + + // @@校验插件脚本解析失败 + File scriptParseFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-plugin-script-parse-error.jar") + .getPath() + ); + assertErrorCode(this.requestMultipart(DEFAULT_ADD, + getDefaultMultiPartParam(request, scriptParseFile)), PLUGIN_SCRIPT_FORMAT); + + // @@校验插件脚本ID重复 + File scriptIdRepeatFile = new File( + this.getClass().getClassLoader().getResource("file/metersphere-plugin-script-id-repeat-error.jar") + .getPath() + ); + assertErrorCode(this.requestMultipart(DEFAULT_ADD, + getDefaultMultiPartParam(request, scriptIdRepeatFile)), PLUGIN_SCRIPT_EXIST); + + request.setGlobal(true); + request.setEnable(true); + request.setName("test2"); + MvcResult antoherMvcResult = this.requestMultipartWithOkAndReturn(DEFAULT_ADD, + getDefaultMultiPartParam(request, anotherJarFile)); + // 校验 global 为 tru e时,organizationIds 为空 + Plugin antoherPlugin = pluginMapper.selectByPrimaryKey(getResultData(antoherMvcResult, Plugin.class).getId()); + Assertions.assertEquals(antoherPlugin.getEnable(), request.getEnable()); + Assertions.assertEquals(antoherPlugin.getGlobal(), request.getGlobal()); + Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(antoherPlugin.getId())); + anotherAddPlugin = antoherPlugin; + this.addPlugin = plugin; - // todo 校验请求成功数据 + // @@校验日志 - // checkLog(this.addPlugin.getId(), OperationLogType.ADD); - // @@异常参数校验 -// createdGroupParamValidateTest(PluginUpdateRequestDefinition.class, ADD); + checkLog(this.addPlugin.getId(), OperationLogType.ADD); // @@校验权限 requestMultipartPermissionTest(PermissionConstants.SYSTEM_PLUGIN_ADD, DEFAULT_ADD, multiValueMap); } - @Test - @Order(1) - public void get() throws Exception { - // @@请求成功 - this.requestGetWithOk(DEFAULT_GET, this.addPlugin.getId()) - .andReturn(); -// Plugin plugin = getResultData(mvcResult, Plugin.class); - // todo 校验数据是否正确 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_GET, this.addPlugin.getId()); - } - @Test @Order(2) - public void getScript() throws Exception { - // @@请求成功 - this.requestGetWithOk(SCRIPT_GET, this.addPlugin.getId(), "script id") - .andReturn(); -// Plugin plugin = getResultData(mvcResult, Plugin.class); - // todo 校验数据是否正确 - // @@校验权限 - requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, SCRIPT_GET, this.addPlugin.getId(), "script id"); - } - - - @Test public void update() throws Exception { // @@请求成功 PluginUpdateRequest request = new PluginUpdateRequest(); + OrganizationDTO org = organizationService.getDefault(); request.setId(addPlugin.getId()); request.setName("test update"); + request.setCreateUser("test update user"); + request.setDescription("test update desc"); + request.setEnable(true); + request.setGlobal(true); + request.setOrganizationIds(Arrays.asList(org.getId())); - MultiValueMap multiValueMap = getDefaultMultiPartParam(request, - new File("src/test/resources/application.properties")); - this.requestMultipartWithOk(DEFAULT_UPDATE, multiValueMap); + this.requestPostWithOk(DEFAULT_UPDATE, request); // 校验请求成功数据 -// Plugin plugin = pluginMapper.selectByPrimaryKey(request.getId()); - // todo 校验请求成功数据 + Plugin plugin = pluginMapper.selectByPrimaryKey(request.getId()); + Assertions.assertEquals(plugin.getName(), request.getName()); + Assertions.assertEquals(plugin.getDescription(), request.getDescription()); + Assertions.assertEquals(plugin.getEnable(), request.getEnable()); + Assertions.assertEquals(plugin.getGlobal(), request.getGlobal()); + Assertions.assertEquals(plugin.getXpack(), false); + // 校验 global 为 true 时,organizationIds 为空 + Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(plugin.getId())); + + // 这些数据不能修改 + Assertions.assertEquals(plugin.getFileName(), addPlugin.getFileName()); + Assertions.assertEquals(plugin.getScenario(), addPlugin.getScenario()); + Assertions.assertEquals(plugin.getCreateUser(), addPlugin.getCreateUser()); + + // 校验 global 为 false 时,organizationIds 数据 + request.setGlobal(false); + this.requestPostWithOk(DEFAULT_UPDATE, request); + Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId())); + + // 校验组织为null,不修改关联关系 + request.setOrganizationIds(null); + this.requestPostWithOk(DEFAULT_UPDATE, request); + Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId())); + + // @@重名校验异常 + request.setName(anotherAddPlugin.getName()); + assertErrorCode( this.requestPost(DEFAULT_UPDATE, request), PLUGIN_EXIST); + // @@校验日志 - // checkLog(request.getId(), OperationLogType.UPDATE); + checkLog(request.getId(), OperationLogType.UPDATE); // @@异常参数校验 -// updatedGroupParamValidateTest(PluginUpdateRequestDefinition.class, UPDATE); + updatedGroupParamValidateTest(PluginUpdateRequestDefinition.class, DEFAULT_UPDATE); // @@校验权限 - requestMultipartPermissionTest(PermissionConstants.SYSTEM_PLUGIN_UPDATE, DEFAULT_UPDATE, multiValueMap); + requestPostPermissionTest(PermissionConstants.SYSTEM_PLUGIN_UPDATE, DEFAULT_UPDATE, request); } @Test + @Order(3) + public void getScript() throws Exception { + // @@请求成功 + MvcResult mvcResult = this.requestGetWithOk(SCRIPT_GET, this.addPlugin.getId(), "connect").andReturn(); + // 校验数据是否正确 + Assertions.assertTrue(StringUtils.isNotBlank(getResultData(mvcResult, String.class))); + // @@校验权限 + requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, SCRIPT_GET, this.addPlugin.getId(), "connect"); + } + + @Test + @Order(4) + public void list() throws Exception { + // @@请求成功 + MvcResult mvcResult = this.requestGetWithOkAndReturn(DEFAULT_LIST); + // 校验数据是否正确 + List pluginList = getResultDataArray(mvcResult, PluginDTO.class); + Assertions.assertEquals(2, pluginList.size()); + for (PluginDTO pluginDTO : pluginList) { + Plugin comparePlugin = null; + if (StringUtils.equals(pluginDTO.getId(), addPlugin.getId())) { + comparePlugin = pluginMapper.selectByPrimaryKey(addPlugin.getId()); + } else if (StringUtils.equals(pluginDTO.getId(), anotherAddPlugin.getId())) { + comparePlugin = pluginMapper.selectByPrimaryKey(anotherAddPlugin.getId()); + } + Plugin plugin = JSON.parseObject(JSON.toJSONString(pluginDTO), Plugin.class); + List scriptIds = pluginDTO.getPluginForms().stream().map(OptionDTO::getId).toList(); + Assertions.assertEquals(plugin, comparePlugin); + Assertions.assertEquals(scriptIds, getScriptIdsByPlugId(plugin.getId())); + List orgList = Optional.ofNullable(pluginDTO.getOrganizations()).orElse(new ArrayList<>(0)); + Assertions.assertEquals(orgList.stream().map(OptionDTO::getId).toList(), getOrgIdsByPlugId(plugin.getId())); + } + + // @@校验权限 + requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_LIST); + } + + @Test + @Order(5) public void delete() throws Exception { // @@请求成功 this.requestGetWithOk(DEFAULT_DELETE, addPlugin.getId()); - // todo 校验请求成功数据 + // 校验请求成功数据 + Plugin plugin = pluginMapper.selectByPrimaryKey(addPlugin.getId()); + Assertions.assertNull(plugin); + Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(addPlugin.getId())); + Assertions.assertEquals(new ArrayList<>(0), getScriptIdsByPlugId(addPlugin.getId())); + // @@校验日志 - // checkLog(addPlugin.getId(), OperationLogType.DELETE); + checkLog(addPlugin.getId(), OperationLogType.DELETE); // @@校验权限 requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_DELETE, DEFAULT_DELETE, addPlugin.getId()); } + + private List getOrgIdsByPlugId(String pluginId) { + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + return pluginOrganizationMapper.selectByExample(example) + .stream() + .map(PluginOrganization::getOrganizationId) + .toList(); + } + + private List getScriptIdsByPlugId(String pluginId) { + PluginScriptExample example = new PluginScriptExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + return pluginScriptMapper.selectByExample(example) + .stream() + .map(PluginScript::getScriptId) + .toList(); + } } \ No newline at end of file diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java index 9467acaef1..f3e886bd79 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java @@ -9,7 +9,7 @@ import lombok.Data; @Data public class PluginUpdateRequestDefinition { @NotBlank(message = "{plugin.id.not_blank}", groups = {Updated.class}) - @Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Created.class, Updated.class}) + @Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Updated.class}) private String id; @NotBlank(groups = {Created.class}) @@ -18,8 +18,4 @@ public class PluginUpdateRequestDefinition { @Size(min = 1, max = 500, groups = {Created.class, Updated.class}) private String description; - - @NotBlank(groups = {Created.class}) - @Size(min = 1, max = 50, groups = {Created.class, Updated.class}) - private String scenario; } diff --git a/backend/services/system-setting/src/test/resources/bootstrap.properties b/backend/services/system-setting/src/test/resources/bootstrap.properties index 62e94d254a..df85de4cef 100644 --- a/backend/services/system-setting/src/test/resources/bootstrap.properties +++ b/backend/services/system-setting/src/test/resources/bootstrap.properties @@ -8,6 +8,6 @@ embedded.mysql.collation=utf8mb4_general_ci # redis embedded.redis.enabled=true # kafka -embedded.kafka.enabled=false +embedded.kafka.enabled=true # minio embedded.minio.enabled=true \ No newline at end of file diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar b/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar new file mode 100644 index 0000000000..f8b4cb2f6f Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar differ diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar b/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar new file mode 100644 index 0000000000..75049c42ff Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar differ diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-repeat-key.jar b/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-repeat-key.jar new file mode 100644 index 0000000000..75049c42ff Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-repeat-key.jar differ diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-id-repeat-error.jar b/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-id-repeat-error.jar new file mode 100644 index 0000000000..23171868f7 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-id-repeat-error.jar differ diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar b/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar new file mode 100644 index 0000000000..7c9cdb1940 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar differ