先写道这,回去写

This commit is contained in:
mianbin 2025-05-08 20:37:25 +08:00
parent 5a5952276a
commit 2276737efa
43 changed files with 905 additions and 22818 deletions

View File

@ -3,6 +3,12 @@
<component name="CopilotChatHistory"> <component name="CopilotChatHistory">
<option name="conversations"> <option name="conversations">
<list> <list>
<Conversation>
<option name="createTime" value="1746706460096" />
<option name="id" value="0196afd16dc0787d9f1e1f1b29555e6c" />
<option name="title" value="新对话 2025年5月08日 20:14:20" />
<option name="updateTime" value="1746706460096" />
</Conversation>
<Conversation> <Conversation>
<option name="createTime" value="1745282575395" /> <option name="createTime" value="1745282575395" />
<option name="id" value="01965af2ac2378d890fe0ac269ec997c" /> <option name="id" value="01965af2ac2378d890fe0ac269ec997c" />

2
.idea/compiler.xml generated
View File

@ -35,7 +35,6 @@
<module name="module-infra-api" /> <module name="module-infra-api" />
<module name="spring-boot-starter-test" /> <module name="spring-boot-starter-test" />
<module name="spring-boot-starter-websocket" /> <module name="spring-boot-starter-websocket" />
<module name="system-plugin-example-web" />
<module name="spring-boot-starter-biz-ip" /> <module name="spring-boot-starter-biz-ip" />
<module name="module-infra-biz" /> <module name="module-infra-biz" />
<module name="module-ci-commons" /> <module name="module-ci-commons" />
@ -62,6 +61,7 @@
<module name="module-ci-store-api" target="17" /> <module name="module-ci-store-api" target="17" />
<module name="module-ci-ticket" target="17" /> <module name="module-ci-ticket" target="17" />
<module name="module-ci-worker" target="17" /> <module name="module-ci-worker" target="17" />
<module name="system-plugin-example-web" target="17" />
</bytecodeTargetLevel> </bytecodeTargetLevel>
</component> </component>
<component name="JavacSettings"> <component name="JavacSettings">

1
.idea/misc.xml generated
View File

@ -75,6 +75,7 @@
<option value="$PROJECT_DIR$/modules/module-ci-store-api/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-store-api/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-ticket/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-ticket/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-worker/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-worker/pom.xml" />
<option value="$PROJECT_DIR$/system-framework/system-plugin-example-web/pom.xml" />
</set> </set>
</option> </option>
</component> </component>

View File

@ -0,0 +1,13 @@
package cd.casic.plugin;
import org.springframework.context.ApplicationContext;
/**
* @Classname PluginApplicationContextFactory
* @Description 创建和刷新插件上下文
* @Date 2025/5/8 19:54
* @Created by mianbin
*/
public interface PluginApplicationContextFactory {
ApplicationContext create(String pluginId);
}

View File

@ -0,0 +1,28 @@
package cd.casic.plugin;
/**
* @Classname PluginConstants
* @Description 常量
* @Date 2025/5/8 14:51
* @Created by mianbin
*/
public class PluginConstants {
/**
* Plugin metadata labels key.
*/
static String PLUGIN_NAME_LABEL_NAME = "plugin.ops/plugin-name";
static String SYSTEM_PLUGIN_NAME = "system";
static String RELOAD_ANNO = "plugin.ops/reload";
static String REQUEST_TO_UNLOAD_LABEL = "plugin.ops/request-to-unload";
static String PLUGIN_PATH = "plugin.ops/plugin-path";
// 模式这边后面要考虑下dev\prod 目前作为现在的情况这两种就够了
static String RUNTIME_MODE_ANNO = "plugin.ops/runtime-mode";
static String assetsRoutePrefix(String pluginName) {
return "/plugins/" + pluginName + "/assets/";
}
}

View File

@ -0,0 +1,16 @@
package cd.casic.plugin;
import cd.casic.plugin.extension.Plugin;
/**
* @Classname PluginGetter
* @Description 抽一个接口
* @Date 2025/5/8 14:58
* @Created by mianbin
*/
@FunctionalInterface
public interface PluginGetter {
Plugin getPlugin(String name);
}

View File

@ -0,0 +1,19 @@
package cd.casic.plugin;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import java.util.Collection;
/**
* @Classname PluginRouterFunctionRegistry
* @Description TODO
* @Date 2025/5/8 19:56
* @Created by mianbin
*/
public interface PluginRouterFunctionRegistry {
void register(Collection<RouterFunction<ServerResponse>> routerFunctions);
void unregister(Collection<RouterFunction<ServerResponse>> routerFunctions);
}

View File

@ -0,0 +1,46 @@
package cd.casic.plugin;
import cd.casic.plugin.extension.Plugin;
import org.pf4j.PluginWrapper;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import reactor.core.publisher.Mono;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Predicate;
/**
* @Classname PluginService
* @Description 插件服务类
* @Date 2025/5/8 19:58
* @Created by mianbin
*/
public interface PluginService {
boolean installPresetPlugins();
Plugin install(Path path);
Plugin upgrade(String name, Path path);
/**
* 重新加载插件插件spec.enabled 设置为 true
*/
Mono<Plugin> reload(String name);
DataBuffer uglifyJsBundle();
DataBuffer uglifyCssBundle();
String generateBundleVersion();
Resource getJsBundle(String version);
Resource getCssBundle(String version);
Plugin changeState(String pluginName, boolean requestToEnable, boolean wait);
List<String> getRequiredDependencies(Plugin plugin,
Predicate<PluginWrapper> predicate);
}

View File

@ -0,0 +1,10 @@
package cd.casic.plugin;
/**
* @Classname SpringPlugin
* @Description TODO
* @Date 2025/5/8 14:48
* @Created by mianbin
*/
public class SpringPlugin {
}

View File

@ -0,0 +1,27 @@
package cd.casic.plugin.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Classname BaseInformation
* @Description 扩展的元数据
* @Date 2025/5/8 15:07
* @Created by mianbin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BasePluginInformation {
String group();
String version();
String kind();
String plural();
String singular();
}

View File

@ -0,0 +1,24 @@
package cd.casic.plugin.event;
import lombok.Getter;
import org.pf4j.PluginWrapper;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* @Classname PluginBeforeStopEvent
* @Description TODO
* @Date 2025/5/8 14:41
* @Created by mianbin
*/
@Getter
public class PluginBeforeStopEvent extends ApplicationEvent {
private final PluginWrapper plugin;
public PluginBeforeStopEvent(Object source, PluginWrapper plugin) {
super(source);
Assert.notNull(plugin, "插件不能为空");
this.plugin = plugin;
}
}

View File

@ -0,0 +1,22 @@
package cd.casic.plugin.event;
import org.pf4j.PluginWrapper;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* @Classname PluginStartedEvent
* @Description 插件启动发布事件到上下文
* @Date 2025/5/8 14:43
* @Created by mianbin
*/
public class PluginStartedEvent extends ApplicationEvent {
private final PluginWrapper plugin;
public PluginStartedEvent(Object source, PluginWrapper plugin) {
super(source);
Assert.notNull(plugin, "插件不能为空");
this.plugin = plugin;
}
}

View File

@ -0,0 +1,22 @@
package cd.casic.plugin.event;
import lombok.Getter;
import org.pf4j.PluginWrapper;
import org.springframework.context.ApplicationEvent;
/**
* @Classname PluginStoppedEvent
* @Description 插件停止触发该事件发布事件到上下文
* @Date 2025/5/8 14:46
* @Created by mianbin
*/
@Getter
public class PluginStoppedEvent extends ApplicationEvent {
private final PluginWrapper plugin;
public PluginStoppedEvent(Object source, PluginWrapper plugin) {
super(source);
this.plugin = plugin;
}
}

View File

@ -0,0 +1,21 @@
package cd.casic.plugin.event;
import cd.casic.plugin.SpringPlugin;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* @Classname SpringPluginStartedEvent
* @Description TODO
* @Date 2025/5/8 14:48
* @Created by mianbin
*/
@Getter
public class SpringPluginStartedEvent extends ApplicationEvent {
private final SpringPlugin springPlugin;
public SpringPluginStartedEvent(Object source, SpringPlugin springPlugin) {
super(source);
this.springPlugin = springPlugin;
}
}

View File

@ -0,0 +1,22 @@
package cd.casic.plugin.event;
import cd.casic.plugin.SpringPlugin;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* @Classname SpringPluginStartingEvent
* @Description TODO
* @Date 2025/5/8 14:49
* @Created by mianbin
*/
@Getter
public class SpringPluginStartingEvent extends ApplicationEvent {
private final SpringPlugin springPlugin;
public SpringPluginStartingEvent(Object source, SpringPlugin springPlugin) {
super(source);
this.springPlugin = springPlugin;
}
}

View File

@ -0,0 +1,22 @@
package cd.casic.plugin.event;
import cd.casic.plugin.SpringPlugin;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* @Classname SpringPluginStoppedEvent
* @Description TODO
* @Date 2025/5/8 14:50
* @Created by mianbin
*/
@Getter
public class SpringPluginStoppedEvent extends ApplicationEvent {
private final SpringPlugin springPlugin;
public SpringPluginStoppedEvent(Object source, SpringPlugin springPlugin) {
super(source);
this.springPlugin = springPlugin;
}
}

View File

@ -0,0 +1,23 @@
package cd.casic.plugin.event;
import cd.casic.plugin.SpringPlugin;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* @Classname SpringPluginStoppingEvent
* @Description TODO
* @Date 2025/5/8 14:50
* @Created by mianbin
*/
@Getter
public class SpringPluginStoppingEvent extends ApplicationEvent {
private final SpringPlugin springPlugin;
public SpringPluginStoppingEvent(Object source, SpringPlugin springPlugin) {
super(source);
this.springPlugin = springPlugin;
}
}

View File

@ -0,0 +1,32 @@
package cd.casic.plugin.extension;
import lombok.Data;
/**
* @Classname AbstractExtension
* @Description TODO
* @Date 2025/5/8 19:59
* @Created by mianbin
*/
@Data
public abstract class AbstractExtension implements Extension {
private String apiVersion;
private String kind;
private MetadataOperator metadata;
@Override
public String getApiVersion() {
var apiVersionFromGvk = Extension.super.getApiVersion();
return apiVersionFromGvk != null ? apiVersionFromGvk : this.apiVersion;
}
@Override
public String getKind() {
var kindFromGvk = Extension.super.getKind();
return kindFromGvk != null ? kindFromGvk : this.kind;
}
}

View File

@ -0,0 +1,26 @@
package cd.casic.plugin.extension;
import java.util.Comparator;
import java.util.Objects;
/**
* @Classname Extension
* @Description TODO
* @Date 2025/5/8 19:33
* @Created by mianbin
*/
public interface Extension extends ExtensionOperator, Comparable<Extension>{
@Override
default int compareTo(Extension another) {
if (another == null || another.getMetadata() == null) {
return 1;
}
if (getMetadata() == null) {
return -1;
}
return Objects.compare(getMetadata().getName(), another.getMetadata().getName(),
Comparator.naturalOrder());
}
}

View File

@ -0,0 +1,74 @@
package cd.casic.plugin.extension;
import cd.casic.plugin.annotation.BasePluginInformation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import kotlin.Metadata;
import org.springframework.util.StringUtils;
import java.util.function.Predicate;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
/**
* @Classname ExtensionOperator
* @Description 扩展类的操作
* @Date 2025/5/8 15:12
* @Created by mianbin
*/
public interface ExtensionOperator {
@Schema(requiredMode = REQUIRED)
@JsonProperty("apiVersion")
default String getApiVersion() {
final var basePluginInformation = getClass().getAnnotation(BasePluginInformation.class);
if (basePluginInformation == null) {
return null;
}
if (StringUtils.hasText(basePluginInformation.group())) {
return basePluginInformation.group() + "/" + basePluginInformation.version();
}
return basePluginInformation.version();
}
@Schema(requiredMode = REQUIRED)
@JsonProperty("kind")
default String getKind() {
final var basePluginInformation = getClass().getAnnotation(BasePluginInformation.class);
if (basePluginInformation == null) {
// return null if having no GVK annotation
return null;
}
return basePluginInformation.kind();
}
@Schema(requiredMode = REQUIRED, implementation = Metadata.class)
@JsonProperty("metadata")
MetadataOperator getMetadata();
void setApiVersion(String apiVersion);
void setKind(String kind);
void setMetadata(MetadataOperator metadata);
default void groupVersionKind(GroupVersionKind gvk) {
setApiVersion(gvk.groupVersion().toString());
setKind(gvk.kind());
}
@JsonIgnore
default GroupVersionKind groupVersionKind() {
return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind());
}
static <T extends ExtensionOperator> Predicate<T> isNotDeleted() {
return ext -> ext.getMetadata().getDeletionTimestamp() == null;
}
static boolean isDeleted(ExtensionOperator extension) {
return ExtensionUtil.isDeleted(extension);
}
}

View File

@ -0,0 +1,69 @@
package cd.casic.plugin.extension;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
/**
* @Classname ExtensionUtil
* @Description TODO
* @Date 2025/5/8 15:12
* @Created by mianbin
*/
public enum ExtensionUtil {
;
public static boolean isDeleted(ExtensionOperator extension) {
return extension.getMetadata() != null
&& extension.getMetadata().getDeletionTimestamp() != null;
}
public static boolean addFinalizers(MetadataOperator metadata, Set<String> finalizers) {
var modifiableFinalizers = new HashSet<>(
metadata.getFinalizers() == null ? Collections.emptySet() : metadata.getFinalizers());
var added = modifiableFinalizers.addAll(finalizers);
if (added) {
metadata.setFinalizers(modifiableFinalizers);
}
return added;
}
public static boolean removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {
if (metadata.getFinalizers() == null) {
return false;
}
var existingFinalizers = new HashSet<>(metadata.getFinalizers());
var removed = existingFinalizers.removeAll(finalizers);
if (removed) {
metadata.setFinalizers(existingFinalizers);
}
return removed;
}
/**
* Query for not deleting.
*
* @return Query
*/
public static boolean notDeleting() {
return StringUtils.isNotBlank("metadata.deletionTimestamp");
}
/**
* Default sort by creation timestamp desc and name asc.
*
* @return Sort
*/
public static Sort defaultSort() {
return Sort.by(
desc("metadata.creationTimestamp"),
asc("metadata.name")
);
}
}

View File

@ -0,0 +1,10 @@
package cd.casic.plugin.extension;
/**
* @Classname GroupKind
* @Description TODO
* @Date 2025/5/8 19:33
* @Created by mianbin
*/
public record GroupKind(String group, String kind) {
}

View File

@ -0,0 +1,30 @@
package cd.casic.plugin.extension;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @Classname GroupVersion
* @Description TODO
* @Date 2025/5/8 19:33
* @Created by mianbin
*/
public record GroupVersion(String group, String version) {
@Override
public String toString() {
return StringUtils.hasText(group) ? group + "/" + version : version;
}
public static GroupVersion parseAPIVersion(String apiVersion) {
Assert.hasText(apiVersion, "API version must not be blank");
var groupVersion = apiVersion.split("/");
return switch (groupVersion.length) {
case 1 -> new GroupVersion("", apiVersion);
case 2 -> new GroupVersion(groupVersion[0], groupVersion[1]);
default ->
throw new IllegalArgumentException("Unexpected APIVersion string: " + apiVersion);
};
}
}

View File

@ -0,0 +1,51 @@
package cd.casic.plugin.extension;
import cd.casic.plugin.annotation.BasePluginInformation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @Classname GroupVersionKind
* @Description TODO
* @Date 2025/5/8 19:33
* @Created by mianbin
*/
public record GroupVersionKind(String group, String version, String kind) {
public GroupVersionKind {
Assert.hasText(version, "Version must not be blank");
Assert.hasText(kind, "Kind must not be blank");
}
public GroupVersion groupVersion() {
return new GroupVersion(group, version);
}
public GroupKind groupKind() {
return new GroupKind(group, kind);
}
public boolean hasGroup() {
return StringUtils.hasText(group);
}
public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String kind) {
Assert.hasText(kind, "Kind must not be blank");
var gv = GroupVersion.parseAPIVersion(apiVersion);
return new GroupVersionKind(gv.group(), gv.version(), kind);
}
public static <T extends Extension> GroupVersionKind fromExtension(Class<T> extension) {
BasePluginInformation gvk = extension.getAnnotation(BasePluginInformation.class);
return new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());
}
@Override
public String toString() {
if (hasGroup()) {
return group + "/" + version + "/" + kind;
}
return version + "/" + kind;
}
}

View File

@ -0,0 +1,38 @@
package cd.casic.plugin.extension;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
/**
* @Classname Metadata
* @Description TODO
* @Date 2025/5/8 19:52
* @Created by mianbin
*/
@Data
@EqualsAndHashCode(exclude = "version")
public class Metadata implements MetadataOperator {
/**
* Metadata name. 唯一的
*/
private String name;
private String generateName;
private Map<String, String> labels;
private Map<String, String> annotations;
private Long version;
private Instant creationTimestamp;
private Instant deletionTimestamp;
private Set<String> finalizers;
}

View File

@ -0,0 +1,101 @@
package cd.casic.plugin.extension;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.v3.oas.annotations.media.Schema;
import kotlin.Metadata;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
/**
* @Classname MetadataOperator
* @Description 元数据的处理
* @Date 2025/5/8 15:17
* @Created by mianbin
*/
@JsonDeserialize(as = Metadata.class)
@Schema(implementation = Metadata.class)
public interface MetadataOperator {
@Schema(name = "name", description = "Metadata name", requiredMode = REQUIRED)
@JsonProperty("name")
String getName();
@Schema(name = "generateName", description = "名字获取失败就自动生成根据generateName字段")
String getGenerateName();
@Schema(name = "labels")
@JsonProperty("labels")
Map<String, String> getLabels();
@Schema(name = "annotations")
@JsonProperty("annotations")
Map<String, String> getAnnotations();
@Schema(name = "version", nullable = true)
@JsonProperty("version")
Long getVersion();
@Schema(name = "creationTimestamp", nullable = true)
@JsonProperty("creationTimestamp")
Instant getCreationTimestamp();
@Schema(name = "deletionTimestamp", nullable = true)
@JsonProperty("deletionTimestamp")
Instant getDeletionTimestamp();
@Schema(name = "finalizers", nullable = true)
Set<String> getFinalizers();
void setName(String name);
void setGenerateName(String generateName);
void setLabels(Map<String, String> labels);
void setAnnotations(Map<String, String> annotations);
void setVersion(Long version);
void setCreationTimestamp(Instant creationTimestamp);
void setDeletionTimestamp(Instant deletionTimestamp);
void setFinalizers(Set<String> finalizers);
static boolean metadataDeepEquals(MetadataOperator left, MetadataOperator right) {
if (left == null && right == null) {
return true;
}
if (left == null || right == null) {
return false;
}
if (!Objects.equals(left.getName(), right.getName())) {
return false;
}
if (!Objects.equals(left.getLabels(), right.getLabels())) {
return false;
}
if (!Objects.equals(left.getAnnotations(), right.getAnnotations())) {
return false;
}
if (!Objects.equals(left.getCreationTimestamp(), right.getCreationTimestamp())) {
return false;
}
if (!Objects.equals(left.getDeletionTimestamp(), right.getDeletionTimestamp())) {
return false;
}
if (!Objects.equals(left.getVersion(), right.getVersion())) {
return false;
}
if (!Objects.equals(left.getFinalizers(), right.getFinalizers())) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,151 @@
package cd.casic.plugin.extension;
import cd.casic.plugin.annotation.BasePluginInformation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.EvictingQueue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.pf4j.PluginState;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
/**
* @Classname Plugin
* @Description TODO
* @Date 2025/5/8 20:03
* @Created by mianbin
*/
@Data
@ToString(callSuper = true)
@BasePluginInformation(group = "plugin.ops", version = "1.0.0", kind = "Plugin", plural = "plugins",
singular = "plugin")
@EqualsAndHashCode(callSuper = true)
public class Plugin extends AbstractExtension {
@Schema(requiredMode = REQUIRED)
private PluginSpec spec;
private PluginStatus status;
@NonNull
@JsonIgnore
public PluginStatus statusNonNull() {
if (this.status == null) {
this.status = new PluginStatus();
}
return status;
}
@Data
public static class PluginSpec {
private String displayName;
@Schema(requiredMode = REQUIRED,
pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
+ ".[0-9a-zA-Z-]+)*))?$")
private String version;
private PluginAuthor author;
private String logo;
private Map<String, String> pluginDependencies = new HashMap<>(4);
private String homepage;
private String repo;
private String issues;
private String description;
private List<License> license;
/**
* SemVer format.
*/
private String requires = "*";
@Deprecated
private String pluginClass;
private Boolean enabled = false;
private String settingName;
private String configMapName;
}
@Data
public static class License {
private String name;
private String url;
}
@Data
public static class PluginStatus {
private Phase phase;
private EvictingQueue queue;
private Instant lastStartTime;
private PluginState lastProbeState;
private String entry;
private String stylesheet;
private String logo;
@Schema(description = "Load location of the plugin, often a path.")
private URI loadLocation;
// 这里实现一个先进先出的队列
public static EvictingQueue nullSafeConditions(@NonNull PluginStatus status) {
Assert.notNull(status, "The status must not be null.");
if (status.getQueue() == null) {
status.setQueue(EvictingQueue.create(20));
}
return status.getQueue();
}
}
public enum Phase {
PENDING,
STARTING,
CREATED,
DISABLING,
DISABLED,
RESOLVED,
STARTED,
STOPPED,
FAILED,
UNKNOWN,
;
}
@Data
@ToString
public static class PluginAuthor {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
private String website;
}
}

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cd.casic.boot</groupId>
<artifactId>system-framework</artifactId>
<version>${revision}</version>
</parent>
<artifactId>system-plugin-example-web</artifactId>
<name>${project.artifactId}</name>
<description>system-plugin-example-web</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>spring-boot-starter-plugin</artifactId>
<version>${revision}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,22 +0,0 @@
package cd.casic.server.system.plugin.example.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Authormianbin
* @Packagecd.casic.opsmodulepluginsexampleweb
* @Projectops
* @nameExampleController
* @Date2024/03/19 16:04
* @FilenameExampleController
* @descriptionTodo
*/
@RestController
public class ExampleController {
@GetMapping("/ok")
public String init() {
return "are u ok!!!";
}
}

View File

@ -1,97 +0,0 @@
package cd.casic.server.system.plugin.example.web;
import cd.casic.plugin.BufferedPluginBundleResource;
import cd.casic.plugin.FrontResourceProcessor;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
@RestController
public class FrontResourceController {
@Resource
BufferedPluginBundleResource bufferedPluginBundleResource;
@Resource
FrontResourceProcessor frontResourceProcessor;
private static final CacheControl MAX_CACHE_CONTROL = CacheControl.maxAge(365, TimeUnit.DAYS);
@GetMapping("/plugins/-/bundle.js")
public ResponseEntity<?> fetchJsBundle(HttpServletRequest request,
@RequestParam(required = false) String v) throws IOException {
if (v != null) {
FileSystemResource fsRes = bufferedPluginBundleResource.getJsBundle(v, frontResourceProcessor.uglifyJsBundle());
Instant lastModified = Instant.ofEpochMilli(fsRes.lastModified());
if (isModified(request, lastModified)) {
HttpHeaders headers = new HttpHeaders();
headers.setCacheControl(MAX_CACHE_CONTROL);
headers.setContentType(MediaType.parseMediaType("application/x-javascript"));
headers.setLastModified(lastModified.toEpochMilli());
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(fsRes.getInputStream()));
} else {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
}
} else {
String newVersion = frontResourceProcessor.generateJsBundleVersion();
String redirectUrl = buildJsBundleUri("js", newVersion).toString();
return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, redirectUrl)
.build();
}
}
@GetMapping("/plugins/-/bundle.css")
public ResponseEntity<?> fetchCssBundle(HttpServletRequest request,
@RequestParam(required = false) String v) throws IOException {
if (v != null) {
FileSystemResource fsRes = bufferedPluginBundleResource.getCssBundle(v, frontResourceProcessor.uglifyCssBundle());
Instant lastModified = Instant.ofEpochMilli(fsRes.lastModified());
if (isModified(request, lastModified)) {
HttpHeaders headers = new HttpHeaders();
headers.setCacheControl(MAX_CACHE_CONTROL);
headers.setContentType(MediaType.parseMediaType("text/css"));
headers.setLastModified(lastModified.toEpochMilli());
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(fsRes.getInputStream()));
} else {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
}
} else {
String newVersion = frontResourceProcessor.generateJsBundleVersion();
String redirectUrl = buildJsBundleUri("css", newVersion).toString();
return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, redirectUrl)
.build();
}
}
private boolean isModified(HttpServletRequest request, Instant lastModified) {
long ifModifiedSince = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
return ifModifiedSince <= 0 || !lastModified.isBefore(Instant.ofEpochMilli(ifModifiedSince));
}
private URI buildJsBundleUri(String type, String version) {
return URI.create(
"/plugins/-/bundle." + type + "?v=" + version);
}
}

View File

@ -1,22 +0,0 @@
package cd.casic.server.system.plugin.example.web;
import org.pf4j.ExtensionPoint;
public interface OperationButtonExtension extends ExtensionPoint {
/**
* 按钮名称
*
* @return 按钮名称
*/
String name();
/**
* 响应点击事件
*
* @return 结果
*/
String onClick();
}

View File

@ -1,13 +0,0 @@
package cd.casic.server.system.plugin.example.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"cd.casic.server.system"})
public class OpsModulePluginsExampleWebApplication {
public static void main(String[] args) {
SpringApplication.run(OpsModulePluginsExampleWebApplication.class, args);
}
}

View File

@ -1,168 +0,0 @@
package cd.casic.server.system.plugin.example.web;
import cd.casic.plugin.PluginDescriptorStorage;
import cd.casic.plugin.PluginManager;
import cd.casic.plugin.PluginProperties;
import cd.casic.plugin.utils.SM4EncryptUtil;
import cd.casic.server.system.plugin.example.web.service.impl.PluginStorageServiceImpl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginState;
import org.pf4j.PluginWrapper;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @Authormianbin
* @Packagecd.casic.opsmodulepluginsexampleweb.controller
* @Projectops
* @namePluginManagerControoler
* @Date2024/03/20 15:05
* @FilenamePluginManagerControoler
* @descriptionTodo
*/
@RestController
@RequestMapping("/plugin")
@Slf4j
public class PluginManagerController {
@Resource
private PluginProperties pluginProperties;
@Resource
private PluginStorageServiceImpl pluginStorageService;
@Resource
private PluginManager pluginManager;
@PostMapping("upload")
public boolean uploadPlugin(@RequestParam("file") MultipartFile file) throws IOException {
InputStream is = file.getInputStream();
Files.copy(is, Paths.get(pluginProperties.getPluginsRoot()).resolve(Objects.requireNonNull(file.getOriginalFilename())));
is.close();
return true;
}
@GetMapping("reload")
public void reloadPlugins() {
pluginManager.unloadPlugins();
pluginManager.loadPlugins();
}
@GetMapping("load")
public String loadPlugin(@RequestParam() String pluginId) {
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
if (plugin != null) {
return pluginManager.loadPlugin(plugin.getPluginPath());
} else {
return "Plugin not found";
}
}
@GetMapping("unload")
public boolean unloadPlugin(@RequestParam() String pluginId) {
return pluginManager.unloadPlugin(pluginId);
}
@GetMapping("start")
public PluginState startPlugin(@RequestParam() String pluginId) {
return pluginManager.startPlugin(pluginId);
}
@GetMapping("stop")
public PluginState stopPlugin(@RequestParam() String pluginId) {
return pluginManager.stopPlugin(pluginId);
}
@GetMapping("disable")
public boolean disablePlugin(@RequestParam() String pluginId) {
this.setPluginDisable(pluginId);
return pluginManager.disablePlugin(pluginId);
}
@GetMapping("enable")
public boolean enablePlugin(@RequestParam() String pluginId) {
this.setPluginEnable(pluginId);
return pluginManager.enablePlugin(pluginId);
}
@GetMapping("delete")
public boolean deletePlugin(@RequestParam() String pluginId) {
return pluginManager.deletePlugin(pluginId);
}
@GetMapping("lists")
public List<JSONObject> listPlugin() {
return getPlugins();
}
@GetMapping("list")
public JSONArray extensions(@RequestParam String extensionClass) throws ClassNotFoundException {
List extensions = pluginManager.getExtensions(Class.forName(extensionClass));
JSONArray result = new JSONArray();
for (Object extension : extensions) {
if (extension instanceof OperationButtonExtension) {
PluginWrapper plugin = pluginManager.whichPlugin(extension.getClass());
result.add(new JSONObject().fluentPut("class", extension.getClass().getName())
.fluentPut("name", ((OperationButtonExtension) extension).name()).fluentPut("plugin", new JSONObject()
.fluentPut("id", plugin.getPluginId())
.fluentPut("version", plugin.getDescriptor().getVersion())
.fluentPut("path", plugin.getPluginPath().toString())
.fluentPut("state", plugin.getPluginState())
.fluentPut("class", plugin.getDescriptor().getPluginClass())));
}
}
return result;
}
@GetMapping("click")
public String click(@RequestParam String extensionClass, @RequestParam String name) throws ClassNotFoundException {
List extensions = pluginManager.getExtensions(Class.forName(extensionClass));
for (Object extension : extensions) {
if (extension instanceof OperationButtonExtension && ((OperationButtonExtension) extension).name().equals(name)) {
return ((OperationButtonExtension) extension).onClick();
}
}
return "Extension not found or unavailable";
}
private List<JSONObject> getPlugins() {
ArrayList<JSONObject> list = new ArrayList<>();
List<PluginDescriptorStorage> pluginDescriptors = pluginStorageService.list();
for (PluginDescriptorStorage pluginDescriptor : pluginDescriptors){
// 校验license
if (SM4EncryptUtil.checkLicense(pluginDescriptor.getLicense(), pluginDescriptor.getPluginId())){
list.add(new JSONObject()
.fluentPut("id", pluginDescriptor.getPluginId())
.fluentPut("version", pluginDescriptor.getVersion())
.fluentPut("path", pluginDescriptor.getPluginDirPath())
.fluentPut("state", pluginManager.getPlugin(pluginDescriptor.getPluginId()).getPluginState())
.fluentPut("class", pluginDescriptor.getPluginClass())
);
}
}
return list;
}
private void setPluginEnable(String pluginId) {
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
PluginDescriptorStorage descriptorStorage = pluginStorageService.convert(plugin);
descriptorStorage.setEnable(1); // 1 是启用
pluginStorageService.updateById(descriptorStorage);
}
private void setPluginDisable(String pluginId) {
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
PluginDescriptorStorage descriptorStorage = pluginStorageService.convert(plugin);
descriptorStorage.setEnable(0); // 0 是禁用
pluginStorageService.updateById(descriptorStorage);
}
}

View File

@ -1,15 +0,0 @@
package cd.casic.server.system.plugin.example.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -1,9 +0,0 @@
package cd.casic.server.system.plugin.example.web.mapper;
import cd.casic.plugin.PluginDescriptorStorage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PluginStorageMapper extends BaseMapper<PluginDescriptorStorage>{
}

View File

@ -1,7 +0,0 @@
package cd.casic.server.system.plugin.example.web.service;
import cd.casic.plugin.PluginDescriptorStorage;
import com.baomidou.mybatisplus.extension.service.IService;
public interface PluginStorageService extends IService<PluginDescriptorStorage> {
}

View File

@ -1,126 +0,0 @@
package cd.casic.server.system.plugin.example.web.service.impl;
import cd.casic.plugin.PluginDescriptorStorage;
import cd.casic.plugin.PluginManager;
import cd.casic.plugin.PluginProperties;
import cd.casic.plugin.event.PluginDirCheckEvent;
import cd.casic.plugin.event.PluginLoadedEvent;
import cd.casic.plugin.event.PluginUnloadedEvent;
import cd.casic.plugin.utils.SM4EncryptUtil;
import cd.casic.server.system.plugin.example.web.mapper.PluginStorageMapper;
import cd.casic.server.system.plugin.example.web.service.PluginStorageService;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginDescriptor;
import org.pf4j.PluginState;
import org.pf4j.PluginWrapper;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@Service
@Slf4j
public class PluginStorageServiceImpl extends ServiceImpl<PluginStorageMapper, PluginDescriptorStorage> implements PluginStorageService {
@Resource
private PluginManager pluginManager;
@Resource
private PluginProperties pluginProperties;
private Boolean Cleaned = false;
@EventListener
public void initializePlugins(ApplicationStartedEvent event) {
pluginManager.unloadPlugins();
Path pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
List<PluginDescriptorStorage> list = this.list();
if (ObjectUtil.isNotEmpty(list)) {
if (!list.get(0).getPluginDirPath().equals(pluginProperties.getPluginsRoot())) {
this.removeBatchByIds(list);
}
list.stream()
.filter(pluginDescriptorStorage -> Paths.get(pluginDescriptorStorage.getPath()).startsWith(pluginRoot)
&& SM4EncryptUtil.checkLicense(pluginDescriptorStorage.getLicense(), pluginDescriptorStorage.getPluginId()))
.forEach(pluginDescriptorStorage -> {
Path pluginPath = Paths.get(pluginDescriptorStorage.getPath());
String pluginId = pluginManager.loadPlugin(pluginPath);
if (PluginDescriptorStorage.EnableStatus.ENABLE.getCode().equals(pluginDescriptorStorage.getEnable())) {
pluginManager.startPlugin(pluginId);
}
});
// }
}
}
@EventListener
public void pluginDirCheck(PluginDirCheckEvent event) throws IOException {
// // 跟之前工作目录路径不同就清空数据库
// String lastDirTxtPath = pluginProperties.getLastDirTxtPath();
// String pluginsRoot = pluginProperties.getPluginsRoot();
// String line = null;
//
// FileReader fileReader = new FileReader(lastDirTxtPath);
// BufferedReader bufferedReader = new BufferedReader(fileReader);
// line = bufferedReader.readLine();
// bufferedReader.close();
// fileReader.close();
//
// if (!line.equals(pluginsRoot)){
// if (!Cleaned){
// this.remove(new LambdaQueryWrapper<PluginDescriptorStorage>());
// Cleaned = true;
// FileWriter fileWriter = new FileWriter(lastDirTxtPath);
// BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
// bufferedWriter.write(pluginsRoot);
// bufferedWriter.flush();
// bufferedWriter.close();
// fileWriter.close();
// }
// }
}
@EventListener
public void insertPlugin(PluginLoadedEvent event) {
PluginWrapper pluginWrapper = event.getPluginInfo().getPluginWrapper();
PluginDescriptorStorage pluginDescriptorStorage = convert(pluginWrapper);
if (this.getById(pluginDescriptorStorage.getPluginId()) != null
|| !SM4EncryptUtil.checkLicense(pluginDescriptorStorage.getLicense()
, pluginDescriptorStorage.getPluginId())) return;
this.save(pluginDescriptorStorage);
}
@EventListener
public void deletePlugin(PluginUnloadedEvent event) {
PluginWrapper pluginWrapper = event.getPluginInfo().getPluginWrapper();
this.remove(new LambdaQueryWrapper<PluginDescriptorStorage>()
.eq(PluginDescriptorStorage::getPluginId, pluginWrapper.getPluginId()));
}
public PluginDescriptorStorage convert(PluginWrapper pluginWrapper) {
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
return PluginDescriptorStorage.builder()
.pluginId(pluginDescriptor.getPluginId())
.pluginClass(pluginDescriptor.getPluginClass())
.pluginDescription(pluginDescriptor.getPluginDescription())
.license(pluginDescriptor.getLicense())
.provider(pluginDescriptor.getProvider())
.version(pluginDescriptor.getVersion())
.dependencies(pluginDescriptor.getDependencies())
.requires(pluginDescriptor.getRequires())
.enable(pluginWrapper.getPluginState().equals(PluginState.DISABLED) ?
PluginDescriptorStorage.EnableStatus.DISABLE.getCode() :
PluginDescriptorStorage.EnableStatus.ENABLE.getCode())
.path(pluginWrapper.getPluginPath().toString())
.pluginDirPath(pluginProperties.getPluginsRoot())
.build();
}
}

View File

@ -1,211 +0,0 @@
server:
port: 8080
spring:
application:
name: ops-plugin
jmx:
enabled: true
thymeleaf:
prefix: classpath:/templates/
encoding: UTF-8
cache: false
suffix: .html
servlet:
content-type: text/html
profiles:
active: dev
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
# allow-bean-definition-overriding: true # Servlet 配置
servlet:
# 文件上传相关配置项
multipart:
max-file-size: 64MB # 单个文件大小
max-request-size: 128MB # 设置总上传的文件大小
enabled: true
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类
# throw-exception-if-no-handler-found: true # 404 错误时抛出异常,方便统一处理
# static-path-pattern: /static/** # 静态资源路径; 注意:如果不配置,则 throw-exception-if-no-handler-found 不生效!!! TODO 芋艿:不能配置,会导致 swagger 不生效
# Jackson 配置项
jackson:
serialization:
write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
fail-on-empty-beans: false # 允许序列化无属性的 Bean
# 数据源配置项
autoconfigure:
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 1 # 初始连接数
min-idle: 1 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: mysql
datasource:
# mysql:
# name: ops-pro
# url: jdbc:mysql://120.48.68.245:13306/${spring.datasource.dynamic.datasource.mysql.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
# username: root
# password: 1qaz!QAZ
mysql:
name: ops_pro
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.mysql.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
username: root
password: 1qaz!QAZ
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:D:/db/sqlitest.db
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
logging:
level:
org.pf4j: debug
ops:
info:
version: 2.0.0
base-package: cd.casic
web:
admin-ui:
url: 127.0.0.1 # Admin 管理后台 UI 的地址
security:
permit-all_urls:
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
websocket:
enable: true # websocket的开关
path: /websocket/message # 路径
maxOnlineCount: 0 # 最大连接人数
sessionMap: true # 保存sessionMap
swagger:
title: Ops平台
description: 提供管理后台、用户 App 的所有功能
version: ${ops.info.version}
url: ${ops.web.admin-ui.url}
license: MIT
codegen:
base-package: ${ops.info.base-package}
db-schemas: ${spring.datasource.dynamic.datasource.master.name}
front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
error-code: # 错误码相关配置项
constants-class-list:
- cd.casic.ops.module.infra.enums.ErrorCodeConstants
- cd.casic.ops.module.member.enums.ErrorCodeConstants
- cd.casic.ops.module.system.enums.ErrorCodeConstants
#- cd.casic.ops.module.mp.enums.ErrorCodeConstants
tenant: # 多租户相关配置项
enable: true
ignore-urls:
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
- /admin-api/system/captcha/get # 获取图片验证码,和租户无关
- /admin-api/system/captcha/check # 校验图片验证码,和租户无关
- /admin-api/infra/file/*/get/** # 获取图片,和租户无关
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
- /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号
- /jmreport/* # 积木报表,无法携带租户编号
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
- /admin-api/leafAlloc/** # id配置接口和租户无关
ignore-tables:
- system_tenant
- system_tenant_package
- system_dict_data
- system_dict_type
- system_error_code
- system_menu
- system_sensitive_word
- system_oauth2_client
- system_notify_template
- infra_codegen_column
- infra_codegen_table
- infra_test_demo
- infra_config
- infra_file_config
- infra_file
- infra_file_content
- infra_job
- infra_job_log
- infra_job_log
- infra_data_source_config
- jimu_dict
- jimu_dict_item
- jimu_report
- jimu_report_data_source
- jimu_report_db
- jimu_report_db_field
- jimu_report_db_param
- jimu_report_link
- jimu_report_map
- jimu_report_share
- rep_demo_dxtj
- rep_demo_employee
- rep_demo_gongsi
- rep_demo_jianpiao
- tmp_report_data_1
- tmp_report_data_income
trade:
order:
app-id: 1 # 商户编号
expire-time: 2h # 支付的过期时间
debug: false
mongo-plus:
data:
mongodb:
host: 127.0.0.1 #ip
port: 27017 #端口
database: learn #数据库名
connectTimeoutMS: 50000 #在超时之前等待连接打开的最长时间(以毫秒为单位)
cd:
casic:
ops:
plugin:
# 测试的时候写相对于工程根目录的相对路径Paths.get就能读到
enable: true
mode: development # 插件运行模式可选值development、deployment
rest-path-prefix: plugins
enable-plugin-id-as-rest-prefix: true
plugins-root: ops\ops-module-plugins\ops-module-plugins-example # 插件根目录
#upload-path: ops-module-plugins\ops-module-plugins-example # 插件jar包上传路径
# plugins-root: ${user.home}/.ops/plugins # 上线之后可以通过环境变量设置固定的路径
springdoc:
swagger-ui:
disable-swagger-default-url: true

View File

@ -1,73 +0,0 @@
-- MySQL dump 10.13 Distrib 8.0.36, for Win64 (x86_64)
--
-- Host: 127.0.0.1 Database: ops_pro
-- ------------------------------------------------------
-- Server version 8.0.36
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `sys_plugins`
--
DROP TABLE IF EXISTS `sys_plugins`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sys_plugins` (
`plugin_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`plugin_description` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`plugin_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`requires` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`dependencies` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`license` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`mapper_xml_dir` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`static_dir` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`plugin_dir_path` varchar(100) DEFAULT NULL,
`enable` tinyint(3) unsigned zerofill NOT NULL COMMENT '是否启用0是禁用1是启用',
`path` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '插件路径,之后要改相对路径',
PRIMARY KEY (`plugin_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-06-14 13:55:37
######################mysql5.7############################
DROP TABLE IF EXISTS `sys_plugins`;
CREATE TABLE `sys_plugins` (
`plugin_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`plugin_description` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`plugin_class` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`requires` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`dependencies` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`license` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`mapper_xml_dir` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`static_dir` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`plugin_dir_path` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`enable` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`path` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
PRIMARY KEY (`plugin_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because one or more lines are too long

View File

@ -1,116 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
<link rel="stylesheet" type="text/css" href="/easyui.css">
<script src="/jquery-2.0.0.min.js"></script>
<script type="text/javascript" src="/jquery.easyui.min.js"></script>
</head>
<body>
<div id="main">
<strong>Plugin Management</strong>
<div style="margin-top: 10px;margin-bottom: 5px">
<form id="uploadForm" method="post" enctype="multipart/form-data" class="easyui-form"
data-options="novalidate:true">
<input type="file" name="file" id="file"/>
<input type="button" onclick="upload()" value="Upload"/>
<input type="button" onclick="reload()" value="Reload"/>
</form>
</div>
<div id="plugins"></div>
<br/>
<br/>
<strong>Business Feature</strong>
<div id="biz" style="margin-top: 5px">
<button onclick="buttonClick('Default Button', 'Default msg')">Default Button</button>
</div>
</div>
</body>
<script>
function extButtonClick(name) {
$.get('plugin/click?extensionClass=cd.casic.ops.example.OperationButtonExtension&name=' + name, function(data) {
$.messager.alert(name, data, 'info');
})
}
function buttonClick(name,msg){
$.messager.alert(name, msg, 'info');
}
function pluginOp(op, pluginId) {
$.get('plugin/' + op + '?pluginId=' + pluginId);
window.location.reload();
}
function upload() {
if (!$('#file').get(0).files[0]) {
$.messager.alert('Info', "Please choose one plugin package", 'warning');
return;
}
$('#uploadForm').form('submit', {
url: "plugin/upload",
success: function (result) {
$.messager.alert('Info', "Please click 'Reload' button to load uploaded plugins", 'info');
}
});
}
function reload() {
$.get('plugin/reload');
window.location.reload();
}
$(document).ready(function () {
$('#plugins').datagrid({
url: 'plugin/lists',
method: 'GET',
striped: true,
fitColumns: true,
pagination: false,
columns: [
[
{field: 'id', title: 'ID'},
{field: 'version', title: 'Version'},
{field: 'path', title: 'Path'},
{field: 'class', title: 'Class'},
{field: 'state', title: 'State'},
{
field: 'action', title: 'Operation', formatter: function (value, row, index) {
var html;
if (row.state === 'FAILED') {
html = '<a href="javascript: void(0);" onclick="pluginOp(\'load\',\'' + row.id + '\')">Load</a>'
} else {
html = '<a href="javascript: void(0);" onclick="pluginOp(\'unload\',\'' + row.id + '\')">Unload</a>';
}
if (row.state === 'RESOLVED' || row.state === 'CREATED' || row.state === 'DISABLED' || row.state === 'STOPPED') {
html += ' <a href="javascript: void(0);" onclick="pluginOp(\'start\',\'' + row.id + '\')">Start</a>'
}
if (row.state === 'STARTED') {
html += ' <a href="javascript: void(0);" onclick="pluginOp(\'stop\',\'' + row.id + '\')">Stop</a>'
}
if (row.state === 'STARTED' || row.state === 'CREATED') {
html += ' <a href="javascript: void(0);" onclick="pluginOp(\'disable\',\'' + row.id + '\')">Disable</a>'
}
if (row.state === 'DISABLED') {
html += ' <a href="javascript: void(0);" onclick="pluginOp(\'enable\',\'' + row.id + '\')">Enable</a>'
}
if (row.state !== 'STARTED') {
html += ' <a href="javascript: void(0);" onclick="pluginOp(\'delete\',\'' + row.id + '\')">Delete</a>'
}
return html;
}
}
]
]
});
$.get('plugin/list?extensionClass=cd.casic.ops.example.OperationButtonExtension', function(data) {
for (let i = 0; i < data.length; i++) {
let ext = data[i];
$('#biz').append(' <button onclick="extButtonClick(\'' + ext.name + '\')">' + ext.name + '</button>');
}
})
});
</script>
</html>