diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f527d5e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 1000 + +[*.{java,c,cpp,kt,xml}] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96b1264 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/**/logs +/**/*.iml +/**/.idea +/**/target +/**/.DS_Store +/**/.njzscloud-dispose diff --git a/njzscloud-common/njzscloud-common-cache/pom.xml b/njzscloud-common/njzscloud-common-cache/pom.xml new file mode 100644 index 0000000..e1a2f1e --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-cache + jar + + njzscloud-common-cache + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + com.njzscloud + njzscloud-common-redis + + + + diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java new file mode 100644 index 0000000..555c690 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Cache.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.cache; + +import java.util.function.Supplier; + +public interface Cache { + T get(String key); + + T get(String key, Supplier supplier); + + T get(String key, long timeout, Supplier supplier); + + void put(String key, Object value, long timeout); + + void put(String key, Object value); + + void remove(String key); +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java new file mode 100644 index 0000000..57fc7ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/Caches.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.cache; + +import cn.hutool.extra.spring.SpringUtil; + +import java.util.function.Supplier; + +public final class Caches { + + public static final Cache CACHE = SpringUtil.getBean(Cache.class); + + public static T get(String key) { + return CACHE.get(key); + } + + public static T get(String key, long timeout, Supplier supplier) { + return CACHE.get(key, timeout, supplier); + } + + public static void put(String key, Object value, long timeout) { + CACHE.put(key, value, timeout); + } + + public static void put(String key, Object value) { + CACHE.put(key, value); + } + + public static void remove(String key) { + CACHE.remove(key); + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java new file mode 100644 index 0000000..943c8ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/DualCache.java @@ -0,0 +1,56 @@ +package com.njzscloud.common.cache; + +import com.njzscloud.common.core.thread.SyncUtil; + +import java.util.function.Supplier; + +public class DualCache implements Cache { + private final Cache FIRST_CACHE; + private final Cache SECOND_CACHE; + + public DualCache(Cache firstCache, Cache secondCache) { + FIRST_CACHE = firstCache; + SECOND_CACHE = secondCache; + } + + @Override + public T get(String key) { + return SyncUtil.syncR(key, () -> FIRST_CACHE.get(key, () -> SECOND_CACHE.get(key))); + } + + @Override + public T get(String key, Supplier supplier) { + return SyncUtil.syncW(key, () -> FIRST_CACHE.get(key, () -> SECOND_CACHE.get(key, supplier))); + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + return SyncUtil.syncW(key, () -> FIRST_CACHE.get(key, timeout, () -> SECOND_CACHE.get(key, timeout, supplier))); + } + + @Override + public void put(String key, Object value) { + SyncUtil.syncW(key, () -> { + FIRST_CACHE.put(key, value); + SECOND_CACHE.put(key, value); + }); + } + + @Override + public void put(String key, Object value, long timeout) { + SyncUtil.syncW(key, () -> { + FIRST_CACHE.put(key, value); + SECOND_CACHE.put(key, value, timeout); + }); + } + + @Override + public void remove(String key) { + SyncUtil.syncW(key, () -> { + FIRST_CACHE.remove(key); + SECOND_CACHE.remove(key); + }); + } +} + + diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java new file mode 100644 index 0000000..cac565f --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/FirstCache.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.cache; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.LFUCache; + +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public class FirstCache implements Cache { + private final LFUCache CACHE; + + public FirstCache(int capacity, long timeout) { + CACHE = CacheUtil.newLFUCache(capacity, timeout); + } + + @Override + public T get(String key) { + return (T) CACHE.get(key); + } + + public T get(String key, Supplier supplier) { + return (T) CACHE.get(key, supplier::get); + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + return (T) CACHE.get(key, true, timeout, supplier::get); + } + + @Override + public void put(String key, Object value, long timeout) { + CACHE.put(key, value, timeout); + } + + @Override + public void put(String key, Object value) { + CACHE.put(key, value); + } + + @Override + public void remove(String key) { + CACHE.remove(key); + } + +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java new file mode 100644 index 0000000..81927f4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/NoCache.java @@ -0,0 +1,36 @@ +package com.njzscloud.common.cache; + +import java.util.function.Supplier; + +public class NoCache implements Cache { + + public NoCache() { + } + + @Override + public T get(String key) { + return null; + } + + public T get(String key, Supplier supplier) { + return supplier.get(); + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + return supplier.get(); + } + + @Override + public void put(String key, Object value, long timeout) { + } + + @Override + public void put(String key, Object value) { + } + + @Override + public void remove(String key) { + } + +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java new file mode 100644 index 0000000..8f8e738 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/SecondCache.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.cache; + +import com.njzscloud.common.redis.util.Redis; +import io.lettuce.core.SetArgs; + +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public class SecondCache implements Cache { + + private final long timeout; + + public SecondCache(long timeout) { + this.timeout = timeout; + } + + public T get(String key) { + return Redis.get(key); + } + + @Override + public T get(String key, Supplier supplier) { + Object o = Redis.get(key); + if (o != null) return (T) o; + o = supplier.get(); + if (o != null) Redis.set(key, o); + return (T) o; + } + + @Override + public T get(String key, long timeout, Supplier supplier) { + Object o = Redis.get(key); + if (o != null) return (T) o; + o = supplier.get(); + if (o != null) Redis.set(key, o, new SetArgs().ex(timeout)); + return (T) o; + } + + public void put(String key, Object value, long timeout) { + Redis.set(key, value, new SetArgs().ex(timeout)); + } + + public void put(String key, Object value) { + if (timeout > 0) Redis.set(key, value, new SetArgs().ex(timeout)); + else Redis.set(key, value); + } + + public void remove(String key) { + Redis.del(key); + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java new file mode 100644 index 0000000..2a90286 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.cache.config; + +import com.njzscloud.common.cache.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration(proxyBeanMethods = false) +@ConditionalOnBooleanProperty(prefix = "cache", name = "enable") +@EnableConfigurationProperties(CacheProperties.class) +public class CacheAutoConfiguration { + + @Bean + public Cache cache(CacheProperties properties) { + CacheProperties.FirstCacheProperties first = properties.getFirst(); + CacheProperties.SecondCacheProperties second = properties.getSecond(); + Cache noCache = new NoCache(); + Cache firstCache = first.isEnabled() ? new FirstCache(first.getCapacity(), first.getTimeout()) : noCache; + Cache secondCache = second.isEnabled() ? new SecondCache(second.getTimeout()) : noCache; + return new DualCache(firstCache, secondCache); + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java new file mode 100644 index 0000000..e995b0f --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/java/com/njzscloud/common/cache/config/CacheProperties.java @@ -0,0 +1,31 @@ +package com.njzscloud.common.cache.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "cache") +public class CacheProperties { + private boolean enabled = false; + + private FirstCacheProperties first = new FirstCacheProperties(); + + private SecondCacheProperties second = new SecondCacheProperties(); + + @Getter + @Setter + public static class FirstCacheProperties { + private boolean enabled = true; + private int capacity = 100000; + private long timeout = 3600 * 24; + } + + @Getter + @Setter + public static class SecondCacheProperties { + private boolean enabled = false; + private long timeout = 3600 * 24; + } +} diff --git a/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..984a0c7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-cache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.cache.config.CacheAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-core/pom.xml b/njzscloud-common/njzscloud-common-core/pom.xml new file mode 100644 index 0000000..100c32b --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-core + + + 21 + 21 + UTF-8 + + + + + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + cn.hutool + hutool-cache + + + cn.hutool + hutool-core + + + cn.hutool + hutool-extra + + + cn.hutool + hutool-captcha + + + cn.hutool + hutool-crypto + + + cn.hutool + hutool-poi + + + + + + org.projectlombok + lombok + + + + + org.slf4j + slf4j-api + + + + + ch.qos.logback + logback-classic + + + + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java new file mode 100644 index 0000000..2a8746e --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/CliException.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 客户端异常, 表示客户端参错误 + */ +public class CliException extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected CliException(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + @Override + public String getMessage() { + return StrUtil.format("客户端错误, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java new file mode 100644 index 0000000..a16a861 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionDepthComparator.java @@ -0,0 +1,93 @@ +package com.njzscloud.common.core.ex; + + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.SimpleCache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + *

用于两个异常之间的排序

+ *

根据当前异常与目标异常在异常继承体系中的层级进行比较

+ * + * @see org.springframework.core.ExceptionDepthComparator + */ +public class ExceptionDepthComparator implements Comparator> { + + /** + * 比较器缓存 + */ + private static final SimpleCache, ExceptionDepthComparator> CACHE = new SimpleCache<>(); + + /** + * 目标异常类型 + */ + private final Class targetException; + + /** + * 创建异常比较器 + * + * @param exception 目标异常 + */ + public ExceptionDepthComparator(Throwable exception) { + Assert.notNull(exception, "目标异常不能为空"); + this.targetException = exception.getClass(); + } + + /** + * 创建异常比较器 + * + * @param exceptionType 目标异常类型 + */ + public ExceptionDepthComparator(Class exceptionType) { + Assert.notNull(exceptionType, "目标异常类型不能为空"); + this.targetException = exceptionType; + } + + /** + * 从给定异常中获取最接近目标异常的匹配项 + * + * @param exceptionTypes 待匹配的异常列表 + * @param targetException 目标异常 + * @return 匹配到的异常 + */ + public static Class findClosestMatch(Collection> exceptionTypes, Throwable targetException) { + Assert.notEmpty(exceptionTypes, "不能为空"); + Assert.notNull(targetException, "不能为空"); + if (exceptionTypes.size() == 1) { + return exceptionTypes.iterator().next(); + } + List> handledExceptions = new ArrayList<>(exceptionTypes); + ExceptionDepthComparator comparator = CACHE.get(targetException.getClass(), () -> new ExceptionDepthComparator(targetException)); + handledExceptions.sort(comparator); + return handledExceptions.get(0); + } + + @Override + public int compare(Class o1, Class o2) { + int depth1 = getDepth(o1, this.targetException); + int depth2 = getDepth(o2, this.targetException); + return depth1 - depth2; + } + + /** + * 获取异常的层级深度 + * + * @param declaredException 待匹配的异常 + * @param exceptionToMatch 目标异常 + * @return 深度(≥0),越近数字越小 + */ + private int getDepth(Class declaredException, Class exceptionToMatch) { + int depth = 0; + do { + if (exceptionToMatch.equals(declaredException)) return depth; + if (exceptionToMatch == Throwable.class) return Integer.MAX_VALUE; + depth++; + exceptionToMatch = exceptionToMatch.getSuperclass(); + } while (true); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java new file mode 100644 index 0000000..c9cbebc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionMsg.java @@ -0,0 +1,53 @@ +package com.njzscloud.common.core.ex; + + +/** + * 异常信息 + *

默认:

+ *

系统异常:SYS_EXP_MSG

+ *

客户端错误:CLI_ERR_MSG

+ *

系统错误:SYS_ERR_MSG

+ */ +public final class ExceptionMsg { + + /** + * 通用系统异常 + */ + public static final ExceptionMsg SYS_EXP_MSG = new ExceptionMsg(ExceptionType.SYS_EXP, 1111, "操作失败"); + + /** + * 通用客户端错误 + */ + public static final ExceptionMsg CLI_ERR_MSG = new ExceptionMsg(ExceptionType.CLI_ERR, 5555, "操作失败"); + + /** + * 通用系统错误 + */ + public static final ExceptionMsg SYS_ERR_MSG = new ExceptionMsg(ExceptionType.SYS_ERR, 9999, "系统错误"); + + /** + * 编号(0< code ≤9999) + */ + public final int code; + /** + * 异常信息(应尽量简略) + */ + public final String msg; + /** + * 异常类型 {@link ExceptionType} + */ + public final ExceptionType type; + + /** + * 创建异常信息 + * + * @param type 异常类型 {@link ExceptionType} + * @param code 异常编号(0< code ≤9999) + * @param msg 异常信息(应尽量简略) + */ + private ExceptionMsg(ExceptionType type, int code, String msg) { + this.code = code; + this.type = type; + this.msg = msg; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java new file mode 100644 index 0000000..3615dfb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExceptionType.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.ex; + +import lombok.RequiredArgsConstructor; + +/** + * 异常类型 + */ +@RequiredArgsConstructor +public enum ExceptionType { + + SYS_EXP(1, "系统异常"), + + CLI_ERR(5, "客户端错误"), + + SYS_ERR(9, "系统错误"); + + public final int code; + + public final String name; + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java new file mode 100644 index 0000000..03e50e9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/Exceptions.java @@ -0,0 +1,77 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 异常工具类 + * 创建异常对象 + */ +public class Exceptions { + + public static SysThrowable clierr(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.CLI_ERR_MSG, message, param); + } + + public static SysThrowable clierr(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.CLI_ERR_MSG, message, null); + } + + public static SysThrowable exception(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_EXP_MSG, message, param); + } + + public static SysThrowable exception(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_EXP_MSG, message, null); + } + + public static SysThrowable error(Object message, Object... param) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, param); + } + + public static SysThrowable error(Object message) { + return build(null, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, null); + } + + public static SysThrowable error(Throwable cause, Object message, Object... param) { + return build(cause, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, param); + } + + public static SysThrowable error(Throwable cause, Object message) { + return build(cause, ExpectData.NULL_DATA, ExceptionMsg.SYS_ERR_MSG, message, null); + } + + /** + *

创建异常对象

+ * + * @param cause 嵌套异常 + * @param expect 期望值 + * @param msg 异常信息 + * @param message 详细信息(字符串模板, 占位符: {}) + * @param param 模板参数 + * @see SysThrowable + * @see ExceptionMsg + * @see ExpectData + */ + private static SysThrowable build(Throwable cause, ExpectData expect, ExceptionMsg msg, Object message, Object[] param) { + + if (message instanceof String + && !StrUtil.isBlank((String) message) + && param != null + && param.length > 0) { + message = StrUtil.format((String) message, param); + } + + SysThrowable sysThrowable; + + if (msg.type == ExceptionType.SYS_EXP) { + sysThrowable = new SysException(cause, expect, msg, message); + } else if (msg.type == ExceptionType.CLI_ERR) { + sysThrowable = new CliException(cause, expect, msg, message); + } else { + sysThrowable = new SysError(cause, expect, msg, message); + } + + return sysThrowable; + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java new file mode 100644 index 0000000..b1f8be2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/ExpectData.java @@ -0,0 +1,60 @@ +package com.njzscloud.common.core.ex; + +import lombok.AllArgsConstructor; + +import java.util.Collections; + +/** + * 期望值 + *

HTTP 响应时 响应体的 JSON 内容

+ */ +@AllArgsConstructor +public enum ExpectData { + + /** + * 响应: null + */ + NULL_DATA(null), + + /** + * 响应: [] + */ + ARR_DATA(Collections.emptyList()), + + /** + * 响应: {} + */ + KV_DATA(Collections.emptyMap()), + + /** + * 响应: "" + */ + STR_BLANK_DATA(""), + + /** + * 响应: 0 + */ + NUM_ZERO_DATA(0), + + /** + * 响应: false + */ + BOOL_FALSE_DATA(Boolean.FALSE), + + /** + * 响应: true + */ + BOOL_TRUE_DATA(Boolean.TRUE), + + /** + * 无响应体 + */ + VOID_DATA(Void.TYPE); + + private final Object data; + + @SuppressWarnings("unchecked") + public T getData() { + return (T) data; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java new file mode 100644 index 0000000..ca6b3df --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysError.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 系统错误, 此类异常无法处理或不应该处理 + */ +public class SysError extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysError(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + + @Override + public String getMessage() { + return StrUtil.format("内部服务错误, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java new file mode 100644 index 0000000..64f1e98 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysException.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.util.StrUtil; + +/** + * 系统异常, 表示可预料的错误或可预料的情况 + */ +public class SysException extends SysThrowable { + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysException(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(cause, expect, msg, message); + } + + @Override + public String getMessage() { + return StrUtil.format("系统异常, 错误码: {}, 错误信息: {}, 详细信息: {}", this.msg.code, this.msg.msg, this.message); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java new file mode 100644 index 0000000..8490214 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ex/SysThrowable.java @@ -0,0 +1,52 @@ +package com.njzscloud.common.core.ex; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.jackson.Jackson; + +/** + * 系统异常 + */ +public abstract class SysThrowable extends RuntimeException { + + /** + * 希望值 + * + * @see ExpectData + */ + public final Object expect; + + /** + * 异常信息(简略) + */ + public final ExceptionMsg msg; + + /** + * 异常信息(详细) + */ + public final Object message; + + /** + * 创建异常 + * + * @param cause 源异常 + * @param expect 期望响应值 + * @param msg 异常信息(简明) + * @param message 异常信息(详细) + */ + protected SysThrowable(Throwable cause, Object expect, ExceptionMsg msg, Object message) { + super(msg.msg, cause); + this.msg = msg; + this.message = message == null ? "" : message; + this.expect = expect; + } + + @Override + public String toString() { + return Jackson.toJsonStr(MapUtil.builder() + .put("code", msg.code) + .put("expect", expect) + .put("msg", msg.msg) + .put("message", message) + .build()); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java new file mode 100644 index 0000000..59cb9f1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/Fastjson.java @@ -0,0 +1,139 @@ +package com.njzscloud.common.core.fastjson; + +import cn.hutool.core.date.DatePattern; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.alibaba.fastjson2.filter.ValueFilter; +import com.alibaba.fastjson2.reader.ObjectReader; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static com.alibaba.fastjson2.JSONReader.Feature.IgnoreCheckClose; + +/** + *

Fastjson 工具

+ *

已开启类型序列化/反序列化支持

+ */ +@Slf4j +public class Fastjson { + private static final JSONWriter.Context JSON_WRITER_CONTEXT; + private static final JSONReader.Context JSON_READER_CONTEXT; + + static { + JSON_WRITER_CONTEXT = new JSONWriter.Context( + JSONWriter.Feature.WriteNulls, // 序列化输出空值字段 + JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数,输出为字符串格式 + JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息 + // JSONWriter.Feature.PrettyFormat, // 格式化输出 + JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString,避免科学计数法 + JSONWriter.Feature.WriteNullListAsEmpty, // 将List类型字段的空值序列化输出为空数组"[]" + JSONWriter.Feature.WriteNullStringAsEmpty, // 将String类型字段的空值序列化输出为空字符串"" + JSONWriter.Feature.WriteLongAsString // 将 Long 作为字符串输出 + ); + + ZoneId zoneId = ZoneId.of("GMT+8"); + + JSON_WRITER_CONTEXT.setZoneId(zoneId); + JSON_WRITER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); + + JSON_WRITER_CONTEXT.configFilter((ValueFilter) (object, name, value) -> { + if (value != null) { + Class clazz = value.getClass(); + if (clazz == LocalDate.class) { + return DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN).withZone(zoneId).format((LocalDate) value); + } else if (clazz == LocalTime.class) { + return DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN).withZone(zoneId).format((LocalTime) value); + } else if (clazz == BigDecimal.class) { + return ((BigDecimal) value).toPlainString(); + } + } + return value; + }); + JSON_READER_CONTEXT = new JSONReader.Context( + JSONReader.Feature.IgnoreSetNullValue, // 忽略输入为null的字段 + JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType + ); + JSON_READER_CONTEXT.setZoneId(zoneId); + JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); + } + + /** + * 序列化为 JSON 字符串 + * + * @param object 数据 + * @return String + * @see JSON#toJSONString(Object, JSONWriter.Context) + */ + public static String toJsonStr(Object object) { + return JSON.toJSONString(object, JSON_WRITER_CONTEXT); + } + + /** + * 序列化为 JSON 字节数组 + * + * @param object 数据 + * @return byte[] + * @see JSON#toJSONBytes(Object, Filter...) + */ + public static byte[] toJsonBytes(Object object) { + return JSON.toJSONBytes(object, StandardCharsets.UTF_8, JSON_WRITER_CONTEXT); + } + + /** + * 反序列化 + * + * @param json JSON 字符串 + * @param type 类型 + * @return T + * @see JSON#parseObject(String, Class, JSONReader.Context) + */ + @SuppressWarnings("unchecked") + public static T toBean(String json, Type type) { + if (json == null || json.isEmpty()) { + return null; + } + return toBean(JSONReader.of(json, JSON_READER_CONTEXT), type); + } + + public static T toBean(InputStream json, Type type) { + if (json == null) { + return null; + } + return toBean(JSONReader.of(json, StandardCharsets.UTF_8, JSON_READER_CONTEXT), type); + + } + + public static T toBean(byte[] json, Type type) { + if (json == null || json.length == 0) { + return null; + } + return toBean(JSONReader.of(json, JSON_READER_CONTEXT), type); + } + + @SuppressWarnings("unchecked") + private static T toBean(JSONReader reader, Type type) { + ObjectReader objectReader = JSON_READER_CONTEXT.getObjectReader(type); + try { + T object = objectReader.readObject(reader, type, null, 0); + reader.handleResolveTasks(object); + if (!reader.isEnd() && (JSON_READER_CONTEXT.getFeatures() & IgnoreCheckClose.mask) == 0) { + throw new JSONException(reader.info("input not end")); + } + return object; + } finally { + reader.close(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java new file mode 100644 index 0000000..548f665 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectDeserializer.java @@ -0,0 +1,107 @@ +package com.njzscloud.common.core.fastjson.serializer; + + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import com.alibaba.fastjson2.util.TypeUtils; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Dict 枚举的 Fastjson 反序列化器
+ * 使用方式(二选一即可):
+ * 1、在字段上使用 JSONField 进行指定
+ * 如,@JSONField(deserializeUsing = DictObjectSerializer.class)
+ * 2、在枚举类上使用 JSONType 进行指定(在接口上指定无效)
+ * 如,@JSONType(writeEnumAsJavaBean = true, deserializer = DictObjectSerializer.class)

+ * JSON 格式见对应的序列化器 {@link DictObjectSerializer} + * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictObjectSerializer + */ +@Slf4j +public class DictObjectDeserializer implements ObjectReader { + + private static final ClassLoader CLASSLOADER = DictObjectDeserializer.class.getClassLoader(); + + @Override + public Dict readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) { + try { + if (jsonReader.isObject()) { + Map map = jsonReader.readObject(); + + String typeField = (String) map.get(IEnum.ENUM_TYPE); + Object valField = map.get(Dict.ENUM_VAL); + + + Class clazz = CLASSLOADER.loadClass(typeField); + + if (valField instanceof String) { + if (DictStr.class.isAssignableFrom(clazz)) { + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse((String) valField, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt((String) valField), constants); + } else { + return null; + } + } else if (valField instanceof Integer) { + if (DictStr.class.isAssignableFrom(clazz)) { + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(String.valueOf(valField), constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse((Integer) valField, constants); + } else { + return null; + } + } else { + return null; + } + } else { + if (jsonReader.isString()) { + Class clazz = TypeUtils.getClass(fieldType); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt(val), constants); + } else { + return null; + } + } else if (jsonReader.isInt()) { + Class clazz = TypeUtils.getClass(fieldType); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = jsonReader.readString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + Integer val = jsonReader.readInt32(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } else { + return null; + } + } + } catch (Exception e) { + log.error("字典枚举反序列化失败", e); + throw Exceptions.error(e, "字典枚举反序列化失败,类型:{},字段名:{}", fieldType, fieldName); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java new file mode 100644 index 0000000..38f1ce2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/fastjson/serializer/DictObjectSerializer.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.core.fastjson.serializer; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.util.TypeUtils; +import com.alibaba.fastjson2.writer.ObjectWriter; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; + +import java.lang.reflect.Type; + +/** + * Dict 枚举的 Fastjson 序列化器
+ * 使用方式(二选一即可):
+ * 1、在字段上使用 JSONField 进行指定
+ * 如,@JSONField(serializeUsing = DictObjectSerializer.class)
+ * 2、在枚举类上使用 JSONType 进行指定(在接口上指定无效)
+ * 如,@JSONType(writeEnumAsJavaBean = true, serializer = DictObjectSerializer.class)

+ * JSON 格式
+ * 1、枚举不是其他对象的属性
+ *
+ * {
+ *   "type": "", // 枚举全限定类名, 反序列化时会用到
+ *   "name": "", // name 属性
+ *   "ordinal": 0, // ordinal 属性
+ *   "val": 1,  // val 属性(字符串/数字), 反序列化时会用到
+ *   "txt": "1" // txt 属性
+ * }
+ * 2、枚举是其他对象的属性
+ *
+ * {
+ *   // ... 其他属性
+ *   "原字段名称": 1, // val 属性(字符串/数字), 反序列化时会用到
+ *   "原字段名称Txt": "1" //  txt 属性
+ * }
+ * + * @see Dict + * @see DictInt + * @see DictStr + */ +public class DictObjectSerializer implements ObjectWriter { + + @Override + public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) { + if (object == null) { + jsonWriter.writeNull(); + return; + } + + if (fieldType == null) { + Dict dict = (Dict) object; + jsonWriter.startObject(); + + jsonWriter.writeName(IEnum.ENUM_TYPE); + jsonWriter.writeColon(); + jsonWriter.writeString(dict.getClass().getName()); + + jsonWriter.writeName(IEnum.ENUM_NAME); + jsonWriter.writeColon(); + jsonWriter.writeString(((Enum) dict).name()); + + jsonWriter.writeName(IEnum.ENUM_ORDINAL); + jsonWriter.writeColon(); + jsonWriter.writeInt32(((Enum) dict).ordinal()); + + jsonWriter.writeName(Dict.ENUM_VAL); + jsonWriter.writeColon(); + jsonWriter.writeAny(dict.getVal()); + + jsonWriter.writeName(Dict.ENUM_TXT); + jsonWriter.writeColon(); + jsonWriter.writeString(dict.getTxt()); + + jsonWriter.endObject(); + } else { + Class clazz = TypeUtils.getClass(fieldType); + + if (DictInt.class.isAssignableFrom(clazz)) { + DictInt dictInt = (DictInt) object; + jsonWriter.writeInt32(dictInt.getVal()); + + jsonWriter.writeName(fieldName + "Txt"); + jsonWriter.writeColon(); + jsonWriter.writeString(dictInt.getTxt()); + } else if (DictStr.class.isAssignableFrom(clazz)) { + DictStr dictStr = (DictStr) object; + jsonWriter.writeString(dictStr.getVal()); + jsonWriter.writeName(fieldName + "Txt"); + jsonWriter.writeColon(); + jsonWriter.writeString(dictStr.getTxt()); + } else { + jsonWriter.writeNull(); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java new file mode 100644 index 0000000..5b4bfe2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/Dict.java @@ -0,0 +1,70 @@ +package com.njzscloud.common.core.ienum; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.core.jackson.serializer.DictDeserializer; +import com.njzscloud.common.core.jackson.serializer.DictSerializer; + +/** + * 字典枚举
+ * 仅两个子接口 DictInt、DictStr
+ * + * @see DictInt + * @see DictStr + * @see DictDeserializer + * @see DictSerializer + * @see DictObjectDeserializer + * @see DictObjectSerializer + */ +@JsonDeserialize(using = DictDeserializer.class) +@JsonSerialize(using = DictSerializer.class) +public interface Dict extends IEnum { + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 val 属性值 + */ + String ENUM_VAL = "val"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 txt 属性值 + */ + String ENUM_TXT = "txt"; + + /** + * 根据 "值" 获取到对应的枚举对象 + * + * @param val 值 + * @param ds 枚举对象数组 + * @param 值类型 + * @param 枚举类型 + * @return Dict + */ + static > D parse(V val, D[] ds) { + for (D d : ds) { + if (d.getVal().equals(val)) { + return d; + } + } + return null; + } + + /** + * 值 + * + * @return T + */ + T getVal(); + + /** + * 文本表示 + * + * @return String + */ + String getTxt(); + + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java new file mode 100644 index 0000000..6e9c5cb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictInt.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.core.ienum; + + +/** + * "值" 类型为 Integer
+ * 枚举应实现此接口 + * + * @see DictStr + */ +public interface DictInt extends Dict { +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java new file mode 100644 index 0000000..345cfac --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/DictStr.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.core.ienum; + +/** + * "值" 类型为 String
+ * 枚举应实现此接口 + * + * @see DictInt + */ +public interface DictStr extends Dict { +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java new file mode 100644 index 0000000..7bf9bb5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/ienum/IEnum.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.core.ienum; + +/** + * 枚举接口 + */ +public interface IEnum { + /** + * 枚举单独序列化时的属性
+ * 存放枚举的全限定类名 + */ + String ENUM_TYPE = "type"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 name 属性值 + */ + String ENUM_NAME = "name"; + + /** + * 枚举单独序列化时的属性
+ * 存放枚举的 ordinal 属性值 + */ + String ENUM_ORDINAL = "ordinal"; +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java new file mode 100644 index 0000000..bbbc3ec --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/Jackson.java @@ -0,0 +1,229 @@ +package com.njzscloud.common.core.jackson; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.extra.spring.SpringUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.serializer.BigDecimalModule; +import com.njzscloud.common.core.jackson.serializer.LongModule; +import com.njzscloud.common.core.jackson.serializer.TimeModule; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Jackson 工具
+ * 从 Spring 中获取 ObjectMapper + */ +@Slf4j +public class Jackson { + private static final ObjectMapper objectMapper; + private static final XmlMapper xmlMapper; + + static { + ObjectMapper temp; + try { + temp = SpringUtil.getBean(ObjectMapper.class); + } catch (Throwable e) { + log.warn("从 Spring 中获取 ObjectMapper 失败"); + temp = createObjectMapper(); + } + objectMapper = temp; + XmlMapper _xmlMapper; + try { + _xmlMapper = SpringUtil.getBean(XmlMapper.class); + } catch (Throwable e) { + log.warn("从 Spring 中获取 XmlMapper 失败"); + _xmlMapper = createXmlMapper(); + } + xmlMapper = _xmlMapper; + } + + + /** + * 序列化为 JSON 字符串 + * + * @param o 数据 + * @return String + */ + public static String toJsonStr(Object o) { + try { + return objectMapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + // log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 序列化为 JSON 字节数组 + * + * @param o 数据 + * @return byte[] + */ + public static byte[] toJsonBytes(Object o) { + try { + return objectMapper.writeValueAsBytes(o); + } catch (JsonProcessingException e) { + // log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 反序列化(泛型支持)
+ * 如: new TypeReference<List<String>>(){}, 类型对象建议缓存 + * + * @param json json JSON 字符串 + * @param type 类型 + * @return T + * @see TypeReference + */ + public static T toBean(String json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (JsonProcessingException e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T toBean(InputStream json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T toBean(byte[] json, Type type) { + try { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + + /** + * 序列化为 JSON 字符串 + * + * @param o 数据 + * @return String + */ + public static String toXmlStr(Object o) { + try { + return xmlMapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + // log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + /** + * 序列化为 JSON 字节数组 + * + * @param o 数据 + * @return byte[] + */ + public static byte[] toXmlBytes(Object o) { + try { + return xmlMapper.writeValueAsBytes(o); + } catch (JsonProcessingException e) { + // log.error("Jackson 序列化失败", e); + throw Exceptions.error(e, "Jackson 序列化失败"); + } + } + + public static T xmlToBean(String json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (JsonProcessingException e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T xmlToBean(InputStream json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + public static T xmlToBean(byte[] json, Type type) { + try { + return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type)); + } catch (Exception e) { + // log.error("Jackson 反序列化失败", e); + throw Exceptions.error(e, "Jackson 反序列化失败"); + } + } + + /** + * 创建 ObjectMapper + * + * @return ObjectMapper + */ + private static ObjectMapper createObjectMapper() { + return new ObjectMapper() + .setLocale(Locale.CHINA) + .setTimeZone(TimeZone.getTimeZone("GMT+8")) + .setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN)) + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + // .configure(SerializationFeature.INDENT_OUTPUT, true) // 格式化输出 + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) + .configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true) + .registerModules(new TimeModule(), new LongModule(), new BigDecimalModule()) + ; + } + + + private static XmlMapper createXmlMapper() { + return XmlMapper.xmlBuilder() + .defaultLocale(Locale.CHINA) + .defaultTimeZone(TimeZone.getTimeZone("GMT+8")) + .defaultDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN)) + .serializationInclusion(JsonInclude.Include.ALWAYS) + .configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .addModules(new TimeModule(), new LongModule(), new BigDecimalModule()) + .build() + ; + } + + /** + * 获取 objectMapper + * + * @return ObjectMapper + */ + public static ObjectMapper objectMapper() { + return objectMapper; + } + + public static XmlMapper xmlMapper() { + return xmlMapper; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java new file mode 100644 index 0000000..a1eabcc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalModule.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.math.BigDecimal; + +/** + * BigDecimal 序列化 + */ +public class BigDecimalModule extends SimpleModule { + { + this.addSerializer(BigDecimal.class, new BigDecimalSerializer()) + .addDeserializer(BigDecimal.class, new NumberDeserializers.BigDecimalDeserializer()); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java new file mode 100644 index 0000000..216e1f2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/BigDecimalSerializer.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * BigDecimal 序列化为字符串 + */ +public class BigDecimalSerializer extends JsonSerializer { + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toPlainString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java new file mode 100644 index 0000000..662e6c8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictDeserializer.java @@ -0,0 +1,124 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * Dict 枚举的 Jackson 反序列化器

+ * JSON 格式见对应的序列化器 {@link DictSerializer} + * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictSerializer + */ +@Slf4j +public class DictDeserializer extends JsonDeserializer { + private static final ClassLoader CLASSLOADER = DictDeserializer.class.getClassLoader(); + + @Override + public Dict deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken currentToken = p.getCurrentToken(); + + if (currentToken == JsonToken.START_OBJECT) { + TreeNode treeNode = p.getCodec().readTree(p); + TreeNode enumType_node = treeNode.get(IEnum.ENUM_TYPE); + String enumTypeField = ((TextNode) enumType_node).textValue(); + TreeNode val_node = treeNode.get(Dict.ENUM_VAL); + + Class clazz; + try { + clazz = CLASSLOADER.loadClass(enumTypeField); + } catch (ClassNotFoundException e) { + throw Exceptions.error(e, "类型加载失败:{}", enumTypeField); + } + if (val_node instanceof TextNode) { + if (DictStr.class.isAssignableFrom(clazz)) { + String val = ((TextNode) val_node).textValue(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + String val = ((TextNode) val_node).textValue(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(Integer.parseInt(val), constants); + } else { + return null; + } + } else if (val_node instanceof IntNode) { + if (DictStr.class.isAssignableFrom(clazz)) { + int val = ((IntNode) val_node).intValue(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(String.valueOf(val), constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + int val = ((IntNode) val_node).intValue(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } else { + return null; + } + + } else { + JsonStreamContext context = p.getParsingContext(); + String currentName = context.getCurrentName(); + + Object currentValue = p.getCurrentValue(); + + Class valueClazz = currentValue.getClass(); + try { + Field[] declaredFields = valueClazz.getDeclaredFields(); + Field field = null; + for (Field field_ : declaredFields) { + if (field_.getName().equals(currentName)) { + field = field_; + break; + } + } + if (field == null) { + declaredFields = valueClazz.getSuperclass().getDeclaredFields(); + for (Field field_ : declaredFields) { + if (field_.getName().equals(currentName)) { + field = field_; + break; + } + } + } + if (field == null) { + throw new NoSuchFieldException("字段不存在:" + currentName); + } + Class clazz = field.getType(); + if (DictStr.class.isAssignableFrom(clazz)) { + String val = p.getValueAsString(); + DictStr[] constants = (DictStr[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictInt.class.isAssignableFrom(clazz)) { + int val = p.getValueAsInt(); + DictInt[] constants = (DictInt[]) clazz.getEnumConstants(); + return Dict.parse(val, constants); + } else { + return null; + } + } catch (Exception e) { + log.error("字典枚举反序列化失败", e); + throw Exceptions.error(e, "字典枚举反序列化失败,字段名:{},值:{}", currentName, currentValue); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java new file mode 100644 index 0000000..cc4b4ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/DictSerializer.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import com.njzscloud.common.core.ienum.IEnum; + +import java.io.IOException; + +/** + * Dict 枚举的 Jackson 序列化器

+ * JSON 格式
+ * 1、枚举不是其他对象的属性
+ *
+ * {
+ *   "type": "", // 枚举全限定类名, 反序列化时会用到
+ *   "name": "", // name 属性
+ *   "ordinal": 0, // ordinal 属性
+ *   "val": 1,  // val 属性(字符串/数字), 反序列化时会用到
+ *   "txt": "1" // txt 属性
+ * }
+ * 2、枚举是其他对象的属性
+ *
+ * {
+ *   // ... 其他属性
+ *   "原字段名称": 1, // val 属性(字符串/数字), 反序列化时会用到
+ *   "原字段名称Txt": "1" //  txt 属性
+ * }
+ * + * @see Dict + * @see DictInt + * @see DictStr + * @see DictDeserializer + */ +public class DictSerializer extends JsonSerializer { + @Override + public void serialize(Dict value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + JsonStreamContext ctx = gen.getOutputContext(); + if (ctx.inRoot()) { + gen.writeStartObject(); + gen.writeStringField(IEnum.ENUM_TYPE, value.getClass().getName()); + gen.writeStringField(IEnum.ENUM_NAME, ((Enum) value).name()); + gen.writeNumberField(IEnum.ENUM_ORDINAL, ((Enum) value).ordinal()); + gen.writeObjectField(Dict.ENUM_VAL, value.getVal()); + gen.writeStringField(Dict.ENUM_TXT, value.getTxt()); + gen.writeEndObject(); + } else { + gen.writeObject(value.getVal()); + String currentName = ctx.getCurrentName(); + gen.writeStringField(currentName + "Txt", value.getTxt()); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java new file mode 100644 index 0000000..59a7d61 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongModule.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; + + +/** + * Long 序列化 + */ +public class LongModule extends SimpleModule { + { + this.addSerializer(Long.class, new LongSerializer()) + .addDeserializer(Long.class, new NumberDeserializers.LongDeserializer(Long.class, null)); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java new file mode 100644 index 0000000..c54a7ad --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/LongSerializer.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.core.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * Long 序列化为字符串 + */ +public class LongSerializer extends JsonSerializer { + @Override + public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java new file mode 100644 index 0000000..633c3dc --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/jackson/serializer/TimeModule.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.core.jackson.serializer; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * 时间类型序列化 + */ +public class TimeModule extends SimpleModule { + { + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) + .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) + .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))) + .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/AnsiColor.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/AnsiColor.java new file mode 100644 index 0000000..fef1ca3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/AnsiColor.java @@ -0,0 +1,113 @@ +package com.njzscloud.common.core.log; + + +import java.util.Arrays; + +/** + * Ansi 颜色 + */ +public enum AnsiColor { + + /** + * 默认前景色 + */ + DEFAULT("39"), + + /** + * 黑 + */ + BLACK("30"), + + /** + * 红 + */ + RED("31"), + + /** + * 绿 + */ + GREEN("32"), + + /** + * 黄 + */ + YELLOW("33"), + + /** + * 蓝 + */ + BLUE("34"), + + /** + * 品红 + */ + MAGENTA("35"), + + /** + * 青 + */ + CYAN("36"), + + /** + * 白 + */ + WHITE("37"), + + /** + * 亮黑(灰) + */ + BRIGHT_BLACK("90"), + + /** + * 亮红 + */ + BRIGHT_RED("91"), + + /** + * 亮绿 + */ + BRIGHT_GREEN("92"), + + /** + * 亮黄 + */ + BRIGHT_YELLOW("93"), + + /** + * 亮蓝 + */ + BRIGHT_BLUE("94"), + + /** + * 亮品红 + */ + BRIGHT_MAGENTA("95"), + + /** + * 亮青 + */ + BRIGHT_CYAN("96"), + + /** + * 亮白 + */ + BRIGHT_WHITE("97"); + + private final String code; + + AnsiColor(String code) { + this.code = code; + } + + public static AnsiColor parse(String name) { + return Arrays.stream(AnsiColor.values()) + .filter(member -> member.toString().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public String toString() { + return this.code; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/ColorConverter.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/ColorConverter.java new file mode 100644 index 0000000..788eea3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/log/ColorConverter.java @@ -0,0 +1,37 @@ +package com.njzscloud.common.core.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.CompositeConverter; + +/** + * 日志颜色转换 + */ +public class ColorConverter extends CompositeConverter { + @Override + protected String transform(ILoggingEvent event, String in) { + AnsiColor color = AnsiColor.parse(getFirstOption()); + if (color == null) { + switch (event.getLevel().toInt()) { + case Level.ERROR_INT: + color = AnsiColor.RED; + break; + case Level.WARN_INT: + color = AnsiColor.YELLOW; + break; + case Level.INFO_INT: + color = AnsiColor.BLUE; + break; + case Level.DEBUG_INT: + color = AnsiColor.GREEN; + break; + case Level.TRACE_INT: + color = AnsiColor.MAGENTA; + break; + default: + color = AnsiColor.BLACK; + } + } + return "\033[" + color + "m" + in + "\033[0m"; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/DefaultThreadFactory.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/DefaultThreadFactory.java new file mode 100644 index 0000000..a8642c5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/DefaultThreadFactory.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.core.thread; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class DefaultThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final boolean virtual; + + public DefaultThreadFactory(boolean virtual, String poolName) { + if (StrUtil.isBlank(poolName)) namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + else namePrefix = poolName + "-"; + this.virtual = virtual; + } + + @Override + public Thread newThread(Runnable r) { + String name = namePrefix + threadNumber.getAndIncrement(); + Runnable task = () -> { + try { + r.run(); + } finally { + threadNumber.decrementAndGet(); + } + }; + Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t1, e) -> log.error("线程异常:{}", t1.getName(), e); + if (virtual) { + return Thread.ofVirtual() + .name(name) + .uncaughtExceptionHandler(uncaughtExceptionHandler) + .unstarted(task); + } else { + return Thread.ofPlatform() + .name(name) + .daemon(true) + .uncaughtExceptionHandler(uncaughtExceptionHandler) + .priority(Thread.NORM_PRIORITY) + .unstarted(task); + } + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java new file mode 100644 index 0000000..e92a563 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/Q.java @@ -0,0 +1,688 @@ +package com.njzscloud.common.core.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Slf4j +@SuppressWarnings("unchecked") +public class Q extends AbstractQueue implements BlockingQueue { + private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); + private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); + + private final int capacity; + private final int standbyCapacity; + private final AtomicInteger count = new AtomicInteger(); + private final AtomicInteger standbyCount = new AtomicInteger(); + + private Node head; + private Node last; + private Node border; + + + public Q() { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public Q(int capacity, int standbyCapacity) { + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + this.standbyCapacity = standbyCapacity; + border = last = head = new Node<>(null); + } + + private void signalNotEmpty() { + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + + private void signalNotFull() { + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + notFull.signal(); + } finally { + putLock.unlock(); + } + } + + private void enqueue(Node node) { + last = last.next = node; + } + + private E dequeue() { + Node h = head; + Node first = h.next; + h.next = h; // help GC + head = first; + E x = first.item; + first.item = null; + return x; + } + + private void fullyLock() { + putLock.lock(); + takeLock.lock(); + } + + private void fullyUnlock() { + takeLock.unlock(); + putLock.unlock(); + } + + public int size() { + return count.get(); + } + + public int remainingCapacity() { + return capacity - count.get(); + } + + public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + int c = -1; + Node node = new Node(e); + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.count; + putLock.lockInterruptibly(); + try { + + while (count.get() == capacity) { + notFull.await(); + } + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + } + + public boolean offer(E e, long timeout, TimeUnit unit) + throws InterruptedException { + + if (e == null) throw new NullPointerException(); + long nanos = unit.toNanos(timeout); + int c = -1; + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.count; + putLock.lockInterruptibly(); + try { + while (count.get() == capacity) { + if (nanos <= 0) + return false; + nanos = notFull.awaitNanos(nanos); + } + Node node = new Node<>(e); + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + return true; + } + + + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + final AtomicInteger count = this.count; + if (count.get() == capacity) return false; + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (count.get() < capacity) { + enqueue(node); + border = node; + c = count.getAndIncrement(); + if (c + 1 < capacity) notFull.signal(); + } + // log.info("放1:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + return c >= 0; + } + + public boolean offerStandby(E e) { + if (e == null) throw new NullPointerException(); + final AtomicInteger count = this.count; + if (standbyCount.get() == standbyCapacity) return false; + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (standbyCount.get() < standbyCapacity) { + enqueue(node); + c = count.get(); + if (c == capacity) { + c--; + standbyCount.getAndIncrement(); + } else { + border = border.next; + c = count.getAndIncrement(); + } + if (c + 1 < capacity) notFull.signal(); + } + // log.info("放2:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + putLock.unlock(); + } + if (c == 0) signalNotEmpty(); + return c >= 0; + } + + public E take() throws InterruptedException { + E x; + int c = -1; + final AtomicInteger count = this.count; + final ReentrantLock takeLock = this.takeLock; + takeLock.lockInterruptibly(); + try { + while (count.get() == 0) { + notEmpty.await(); + } + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + // log.info("取2:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + takeLock.unlock(); + } + if (c == capacity) signalNotFull(); + return x; + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E x = null; + int c = -1; + long nanos = unit.toNanos(timeout); + final AtomicInteger count = this.count; + final ReentrantLock takeLock = this.takeLock; + takeLock.lockInterruptibly(); + try { + while (count.get() == 0) { + if (nanos <= 0) + return null; + nanos = notEmpty.awaitNanos(nanos); + } + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + // log.info("取1:窗口数:{},备份数:{}", count.get(), standbyCount.get()); + } finally { + takeLock.unlock(); + } + if (c == capacity) + signalNotFull(); + return x; + } + + public E poll() { + final AtomicInteger count = this.count; + if (count.get() == 0) + return null; + E x = null; + int c = -1; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + if (count.get() > 0) { + if (border.next == null) { + c = count.getAndDecrement(); + } else { + border = border.next; + standbyCount.getAndDecrement(); + c = count.get(); + } + x = dequeue(); + if (c > 1) notEmpty.signal(); + } + } finally { + takeLock.unlock(); + } + if (c == capacity) + signalNotFull(); + return x; + } + + public E peek() { + if (count.get() == 0) return null; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + Node first = head.next; + if (first == null) return null; + else return first.item; + } finally { + takeLock.unlock(); + } + } + + void unlink(Node trail, Node p, int borderFlag) { + p.item = null; + trail.next = p.next; + if (last == p) last = trail; + if (borderFlag == 0) { + if (last == p) { + border = trail; + if (count.getAndDecrement() == capacity) notFull.signal(); + } else { + border = p.next; + } + } else if (borderFlag == -1) { + if (last == border) { + if (count.getAndDecrement() == capacity) notFull.signal(); + } else { + border = border.next; + } + } else if (borderFlag == 1) { + standbyCount.getAndDecrement(); + } + } + + public boolean remove(Object o) { + if (o == null) return false; + fullyLock(); + try { + // -1-->左边、0-->边界、1-->右边 + int borderFlag = -1; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (p == border) { + borderFlag = 0; + } else if (borderFlag == 0) { + borderFlag = 1; + } + if (o.equals(p.item)) { + unlink(trail, p, borderFlag); + return true; + } + } + return false; + } finally { + fullyUnlock(); + } + } + + public boolean contains(Object o) { + if (o == null) return false; + fullyLock(); + try { + for (Node p = head.next; p != null; p = p.next) + if (o.equals(p.item)) + return true; + return false; + } finally { + fullyUnlock(); + } + } + + public Object[] toArray() { + fullyLock(); + try { + int size = count.get() + standbyCount.get(); + Object[] a = new Object[size]; + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = p.item; + return a; + } finally { + fullyUnlock(); + } + } + + public T[] toArray(T[] a) { + fullyLock(); + try { + int size = count.get() + standbyCount.get(); + if (a.length < size) + a = (T[]) java.lang.reflect.Array.newInstance + (a.getClass().getComponentType(), size); + + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = (T) p.item; + if (a.length > k) + a[k] = null; + return a; + } finally { + fullyUnlock(); + } + } + + public String toString() { + fullyLock(); + try { + Node p = head.next; + if (p == null) + return "[]"; + + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (; ; ) { + E e = p.item; + sb.append(e == this ? "(this Collection)" : e); + p = p.next; + if (p == null) + return sb.append(']').toString(); + sb.append(',').append(' '); + } + } finally { + fullyUnlock(); + } + } + + public void clear() { + fullyLock(); + try { + for (Node p, h = head; (p = h.next) != null; h = p) { + h.next = h; + p.item = null; + } + head = last; + // assert head.item == null && head.next == null; + standbyCount.getAndSet(0); + if (count.getAndSet(0) == capacity) + notFull.signal(); + } finally { + fullyUnlock(); + } + } + + public int drainTo(Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + public int drainTo(Collection c, int maxElements) { + if (c == null) + throw new NullPointerException(); + if (c == this) + throw new IllegalArgumentException(); + if (maxElements <= 0) return 0; + boolean signalNotFull = false; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + int n = Math.min(maxElements, count.get() + standbyCount.get()); + // count.get provides visibility to first n Nodes + Node h = head; + int i = 0; + int x = 0; + int y = 0; + try { + while (i < n) { + Node p = h.next; + c.add(p.item); + if (border == last) { + x++; + } else { + border = border.next; + y++; + } + p.item = null; + h.next = h; + h = p; + ++i; + } + return n; + } finally { + head = h; + if (x > 0) { + signalNotFull = (count.getAndAdd(-x) == capacity); + } + if (y > 0) { + standbyCount.getAndAdd(-y); + } + } + } finally { + takeLock.unlock(); + if (signalNotFull) + signalNotFull(); + } + } + + public Iterator iterator() { + return new Itr(); + } + + public Spliterator spliterator() { + return new LBQSpliterator(this); + } + + static class Node { + E item; + + Node next; + + Node(E x) { + item = x; + } + } + + static final class LBQSpliterator implements Spliterator { + static final int MAX_BATCH = 1 << 25; // max batch array size; + final Q queue; + Node current; // current node; null until initialized + int batch; // batch size for splits + boolean exhausted; // true when no more nodes + long est; // size estimate + + LBQSpliterator(Q queue) { + this.queue = queue; + this.est = queue.size(); + } + + public long estimateSize() { + return est; + } + + public Spliterator trySplit() { + Node h; + final Q q = this.queue; + int b = batch; + int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1; + if (!exhausted && + ((h = current) != null || (h = q.head.next) != null) && + h.next != null) { + Object[] a = new Object[n]; + int i = 0; + Node p = current; + q.fullyLock(); + try { + if (p != null || (p = q.head.next) != null) { + do { + if ((a[i] = p.item) != null) + ++i; + } while ((p = p.next) != null && i < n); + } + } finally { + q.fullyUnlock(); + } + if ((current = p) == null) { + est = 0L; + exhausted = true; + } else if ((est -= i) < 0L) + est = 0L; + if (i > 0) { + batch = i; + return Spliterators.spliterator + (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT); + } + } + return null; + } + + public void forEachRemaining(Consumer action) { + if (action == null) throw new NullPointerException(); + final Q q = this.queue; + if (!exhausted) { + exhausted = true; + Node p = current; + do { + E e = null; + q.fullyLock(); + try { + if (p == null) + p = q.head.next; + while (p != null) { + e = p.item; + p = p.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (e != null) + action.accept(e); + } while (p != null); + } + } + + public boolean tryAdvance(Consumer action) { + if (action == null) throw new NullPointerException(); + final Q q = this.queue; + if (!exhausted) { + E e = null; + q.fullyLock(); + try { + if (current == null) + current = q.head.next; + while (current != null) { + e = current.item; + current = current.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (current == null) + exhausted = true; + if (e != null) { + action.accept(e); + return true; + } + } + return false; + } + + public int characteristics() { + return Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT; + } + } + + private class Itr implements Iterator { + + private Node current; + private Node lastRet; + private E currentElement; + + Itr() { + fullyLock(); + try { + current = head.next; + if (current != null) + currentElement = current.item; + } finally { + fullyUnlock(); + } + } + + public boolean hasNext() { + return current != null; + } + + private Node nextNode(Node p) { + for (; ; ) { + Node s = p.next; + if (s == p) + return head.next; + if (s == null || s.item != null) + return s; + p = s; + } + } + + public E next() { + fullyLock(); + try { + if (current == null) + throw new NoSuchElementException(); + E x = currentElement; + lastRet = current; + current = nextNode(current); + currentElement = (current == null) ? null : current.item; + return x; + } finally { + fullyUnlock(); + } + } + + public void remove() { + if (lastRet == null) + throw new IllegalStateException(); + fullyLock(); + try { + Node node = lastRet; + lastRet = null; + // -1-->左边、0-->边界、1-->右边 + int borderFlag = -1; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (p == border) { + borderFlag = 0; + } else if (borderFlag == 0) { + borderFlag = 1; + } + if (p == node) { + unlink(trail, p, borderFlag); + break; + } + } + } finally { + fullyUnlock(); + } + } + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/SyncUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/SyncUtil.java new file mode 100644 index 0000000..8e8e9c7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/SyncUtil.java @@ -0,0 +1,385 @@ +package com.njzscloud.common.core.thread; + +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ex.SysThrowable; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class SyncUtil { + + public static void sync(String blockName, long timeout, boolean fair, SyncBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.get(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unlock(); + } + } + } + + public static T sync(String blockName, long timeout, boolean fair, SyncSupplierBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.get(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + return syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unlock(); + } + } + } + + public static T sync(String blockName, boolean fair, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, fair, syncBlock); + } + + public static T sync(String blockName, long timeout, SyncSupplierBlock syncBlock) { + return sync(blockName, timeout, false, syncBlock); + } + + public static T sync(String blockName, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, false, syncBlock); + } + + public static void sync(String blockName, boolean fair, SyncBlock syncBlock) { + sync(blockName, 0, fair, syncBlock); + } + + public static void sync(String blockName, long timeout, SyncBlock syncBlock) { + sync(blockName, timeout, false, syncBlock); + } + + public static void sync(String blockName, SyncBlock syncBlock) { + sync(blockName, 0, false, syncBlock); + } + + public static void syncR(String blockName, long timeout, boolean fair, SyncBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.getRLock(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unrlock(); + } + } + } + + public static T syncR(String blockName, long timeout, boolean fair, SyncSupplierBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.getRLock(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + return syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unrlock(); + } + } + } + + public static T syncR(String blockName, boolean fair, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, fair, syncBlock); + } + + public static T syncR(String blockName, long timeout, SyncSupplierBlock syncBlock) { + return sync(blockName, timeout, false, syncBlock); + } + + public static T syncR(String blockName, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, false, syncBlock); + } + + public static void syncR(String blockName, boolean fair, SyncBlock syncBlock) { + sync(blockName, 0, fair, syncBlock); + } + + public static void syncR(String blockName, long timeout, SyncBlock syncBlock) { + sync(blockName, timeout, false, syncBlock); + } + + public static void syncR(String blockName, SyncBlock syncBlock) { + sync(blockName, 0, false, syncBlock); + } + + public static void syncW(String blockName, long timeout, boolean fair, SyncBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.getWLock(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unwlock(); + } + } + } + + public static T syncW(String blockName, long timeout, boolean fair, SyncSupplierBlock syncBlock) { + LockWrapper lockWrapper = null; + try { + lockWrapper = LockWrapper.getWLock(blockName, timeout, fair); + if (lockWrapper != null) { + // 加锁成功 + return syncBlock.block(); + } else { + throw Exceptions.error("锁获取失败:锁名称:{}", blockName); + } + } catch (SysThrowable e) { + throw e; + } catch (Exception e) { + throw Exceptions.error(e, "同步块执行失败:锁名称:{}", blockName); + } finally { + if (lockWrapper != null) { + lockWrapper.unwlock(); + } + } + } + + public static T syncW(String blockName, boolean fair, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, fair, syncBlock); + } + + public static T syncW(String blockName, long timeout, SyncSupplierBlock syncBlock) { + return sync(blockName, timeout, false, syncBlock); + } + + public static T syncW(String blockName, SyncSupplierBlock syncBlock) { + return sync(blockName, 0, false, syncBlock); + } + + public static void syncW(String blockName, boolean fair, SyncBlock syncBlock) { + sync(blockName, 0, fair, syncBlock); + } + + public static void syncW(String blockName, long timeout, SyncBlock syncBlock) { + sync(blockName, timeout, false, syncBlock); + } + + public static void syncW(String blockName, SyncBlock syncBlock) { + sync(blockName, 0, false, syncBlock); + } + + public interface SyncBlock { + void block(); + } + + public interface SyncSupplierBlock { + T block(); + } + + private static class LockWrapper { + private static final Map LOCKS = new ConcurrentHashMap<>(); + private final String blockName; + private final ReentrantLock lock; + private final ReentrantReadWriteLock rwlock; + private int count; + + private LockWrapper(String blockName, boolean fair) { + this(blockName, fair, false); + } + + private LockWrapper(String blockName, boolean fair, boolean rw) { + if (rw) { + rwlock = new ReentrantReadWriteLock(fair); + lock = null; + } else { + lock = new ReentrantLock(fair); + rwlock = null; + } + this.count = 0; + this.blockName = blockName; + } + + public static LockWrapper getRLock(String blockName, long timeout, boolean fair) { + LockWrapper lockWrapper = LOCKS.compute(blockName, (k, v) -> { + LockWrapper wrapper = Objects.requireNonNullElseGet(v, () -> new LockWrapper(blockName, fair, true)); + // 增加引用数 + wrapper.count++; + return wrapper; + }); + if (timeout > 0) { + if (!lockWrapper.tryRLock(timeout)) { + // 加锁失败,回退引用数 + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + return null; + } else { + return lockWrapper; + } + } else { + lockWrapper.rlock(); + return lockWrapper; + } + } + + public static LockWrapper getWLock(String blockName, long timeout, boolean fair) { + LockWrapper lockWrapper = LOCKS.compute(blockName, (k, v) -> { + LockWrapper wrapper = Objects.requireNonNullElseGet(v, () -> new LockWrapper(blockName, fair, true)); + // 增加引用数 + wrapper.count++; + return wrapper; + }); + if (timeout > 0) { + if (!lockWrapper.tryWLock(timeout)) { + // 加锁失败,回退引用数 + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + return null; + } else { + return lockWrapper; + } + } else { + lockWrapper.wlock(); + return lockWrapper; + } + } + + public static LockWrapper get(String blockName, long timeout, boolean fair) { + LockWrapper lockWrapper = LOCKS.compute(blockName, (k, v) -> { + LockWrapper wrapper = Objects.requireNonNullElseGet(v, () -> new LockWrapper(blockName, fair)); + // 增加引用数 + wrapper.count++; + return wrapper; + }); + if (timeout > 0) { + if (!lockWrapper.tryLock(timeout)) { + // 加锁失败,回退引用数 + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + return null; + } else { + return lockWrapper; + } + } else { + lockWrapper.lock(); + return lockWrapper; + } + } + + private void rlock() { + rwlock.readLock().lock(); + } + + private boolean tryRLock(long timeout) { + try { + return rwlock.readLock().tryLock(timeout, TimeUnit.MICROSECONDS); + } catch (InterruptedException e) { + return false; + } + } + + private void wlock() { + rwlock.writeLock().lock(); + } + + private boolean tryWLock(long timeout) { + try { + return rwlock.writeLock().tryLock(timeout, TimeUnit.MICROSECONDS); + } catch (InterruptedException e) { + return false; + } + } + + private void lock() { + lock.lock(); + } + + private boolean tryLock(long timeout) { + try { + return lock.tryLock(timeout, TimeUnit.MICROSECONDS); + } catch (InterruptedException e) { + return false; + } + } + + public void unrlock() { + // 正常解锁 + rwlock.readLock().unlock(); + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + } + + public void unwlock() { + // 正常解锁 + rwlock.writeLock().unlock(); + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + } + + private void unlock() { + lock.unlock(); + LOCKS.compute(blockName, (k, v) -> { + if (v == null) return null; + v.count--; + return v.count > 0 ? v : null; + }); + } + + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadContext.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadContext.java new file mode 100644 index 0000000..2db6fdb --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadContext.java @@ -0,0 +1,63 @@ +package com.njzscloud.common.core.thread; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@SuppressWarnings("unchecked") +public class ThreadContext { + private static final Map> CONTEXT_CACHE = new ConcurrentHashMap<>(); + + /** + * 获取 + * + * @param key 键 + * @param defaultValue 默认值 + */ + public static T get(String key, T defaultValue) { + return (T) CONTEXT_CACHE + .computeIfAbsent(Thread.currentThread(), k -> new ConcurrentHashMap<>()) + .computeIfAbsent(key, k -> defaultValue); + } + + /** + * 获取 + * + * @param key 键 + */ + public static T get(String key) { + return (T) CONTEXT_CACHE + .computeIfAbsent(Thread.currentThread(), k -> new ConcurrentHashMap<>()) + .get(key); + } + + /** + * 保存 + * + * @param key 键 + * @param value 值 + */ + public static T set(String key, T value) { + return (T) CONTEXT_CACHE + .computeIfAbsent(Thread.currentThread(), k -> new ConcurrentHashMap<>()) + .put(key, value); + } + + /** + * 移除 + * + * @param key 键 + */ + public static void remove(String key) { + CONTEXT_CACHE + .computeIfAbsent(Thread.currentThread(), k -> new ConcurrentHashMap<>()) + .remove(key); + } + + /** + * 清理当前线程对应的数据 + */ + public static void clear() { + CONTEXT_CACHE.remove(Thread.currentThread()); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java new file mode 100644 index 0000000..030b94f --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/ThreadPool.java @@ -0,0 +1,108 @@ +package com.njzscloud.common.core.thread; + + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.*; +import java.util.function.Supplier; + +@Slf4j +public class ThreadPool { + private static volatile ThreadPoolExecutor THREAD_POOL; + + /** + * 默认线程池(虚拟线程) + */ + public static ThreadPoolExecutor defaultThreadPool() { + if (THREAD_POOL == null) { + synchronized (ThreadPool.class) { + if (THREAD_POOL == null) { + THREAD_POOL = createThreadPool("Default"); + } + } + } + return THREAD_POOL; + } + + /** + * 创建线程池(平台线程) + * + * @param poolName 线程池名称 + * @param poolSize 线程数量 + * @param bufferSize 队列缓冲区大小 + */ + public static ThreadPoolExecutor createThreadPool(String poolName, int poolSize, int bufferSize) { + return createPlatformThreadPool(poolName, 0, poolSize, 60, bufferSize, Integer.MAX_VALUE, null); + } + + /** + * 创建线程池(虚拟线程) + * + * @param poolName 线程池名称 + */ + public static ThreadPoolExecutor createThreadPool(String poolName) { + return createVirtualThreadPool(poolName, null); + } + + + /** + * 创建线程池(虚拟线程) + * + * @param poolName 线程池名称 + * @param rejectedExecutionHandler 拒绝策略 + */ + public static ThreadPoolExecutor createVirtualThreadPool(String poolName, RejectedExecutionHandler rejectedExecutionHandler) { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + 0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, + new SynchronousQueue<>(), + new DefaultThreadFactory(true, poolName), + rejectedExecutionHandler == null ? new ThreadPoolExecutor.AbortPolicy() : rejectedExecutionHandler); + threadPoolExecutor.allowCoreThreadTimeOut(true); + return threadPoolExecutor; + } + + + /** + * + * @param poolName 线程池名称 + * @param corePoolSize 核心线程数 + * @param maxPoolSize 最大线程数 + * @param keepAliveSeconds 空闲时间(秒) + * @param windowCapacity 一级队列大小 + * @param standbyCapacity 二级队列大小 + * @param rejectedExecutionHandler 拒绝策略 + */ + public static ThreadPoolExecutor createPlatformThreadPool(String poolName, + int corePoolSize, int maxPoolSize, long keepAliveSeconds, + int windowCapacity, int standbyCapacity, + RejectedExecutionHandler rejectedExecutionHandler + ) { + Assert.isTrue(windowCapacity > 0 && standbyCapacity > 0 && windowCapacity <= standbyCapacity, () -> Exceptions.error("队列大小设置错误")); + Q q = new Q<>(windowCapacity, standbyCapacity); + RejectedExecutionHandler abortPolicy = rejectedExecutionHandler == null ? new ThreadPoolExecutor.AbortPolicy() : rejectedExecutionHandler; + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + corePoolSize, maxPoolSize, keepAliveSeconds <= 0 ? 60 : keepAliveSeconds, TimeUnit.SECONDS, + q, + new DefaultThreadFactory(false, poolName), + (r, p) -> { + if (!q.offerStandby(r)) { + log.debug("任务队列已满"); + abortPolicy.rejectedExecution(r, p); + } else { + log.debug("任务已加入备用队列"); + } + }); + threadPoolExecutor.allowCoreThreadTimeOut(corePoolSize == 0); + return threadPoolExecutor; + } + + public static CompletableFuture runAsync(Runnable runnable) { + return CompletableFuture.runAsync(runnable, defaultThreadPool()); + } + + public static CompletableFuture supplyAsync(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, defaultThreadPool()); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java new file mode 100644 index 0000000..0f2d958 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/thread/WindowBlockingQueue.java @@ -0,0 +1,693 @@ +package com.njzscloud.common.core.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Slf4j +@SuppressWarnings("unchecked") +public class WindowBlockingQueue extends AbstractQueue implements BlockingQueue { + + private final int windowCapacity; + private final int totalCapacity; + private final AtomicInteger totalElementCount = new AtomicInteger(); + private final AtomicInteger windowElementCount = new AtomicInteger(); + private final AtomicInteger standbyElementCount = new AtomicInteger(); + private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); + private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); + private Node head; + private Node border; + private Node last; + + public WindowBlockingQueue() { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public WindowBlockingQueue(int totalCapacity, int windowCapacity) { + if (windowCapacity <= 0 || totalCapacity < windowCapacity) throw new IllegalArgumentException(); + this.totalCapacity = totalCapacity; + this.windowCapacity = windowCapacity; + head = border = last = new Node<>(null); + } + + public WindowBlockingQueue(Collection c) { + this(Integer.MAX_VALUE, Integer.MAX_VALUE); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + int n = 0; + for (E e : c) { + if (e == null) + throw new NullPointerException(); + if (n == windowCapacity) + throw new IllegalStateException("Queue full"); + enqueue(new Node<>(e)); + ++n; + } + windowElementCount.set(n); + } finally { + putLock.unlock(); + } + } + + private void signalNotEmpty() { + takeLock.lock(); + try { + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + + private void signalNotFull() { + putLock.lock(); + try { + notFull.signal(); + } finally { + putLock.unlock(); + } + } + + private void enqueue(Node node) { + last = last.next = node; + totalElementCount.incrementAndGet(); + } + + private E dequeue() { + Node h = head; + Node first = h.next; + h.next = h; + head = first; + E x = first.item; + first.item = null; + totalElementCount.decrementAndGet(); + return x; + } + + void fullyLock() { + putLock.lock(); + takeLock.lock(); + } + + void fullyUnlock() { + takeLock.unlock(); + putLock.unlock(); + } + + public int size() { + return windowElementCount.get(); + } + + public int remainingCapacity() { + return windowCapacity - windowElementCount.get(); + } + + public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + int c = -1; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.windowElementCount; + putLock.lockInterruptibly(); + try { + while (count.get() == windowCapacity) { + notFull.await(); + } + enqueue(node); + c = count.getAndIncrement(); + if (c + 1 < windowCapacity) + notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + } + + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + if (e == null) throw new NullPointerException(); + long nanos = unit.toNanos(timeout); + int c = -1; + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.windowElementCount; + putLock.lockInterruptibly(); + try { + while (count.get() == windowCapacity) { + if (nanos <= 0) + return false; + nanos = notFull.awaitNanos(nanos); + } + enqueue(new Node(e)); + c = count.getAndIncrement(); + if (c + 1 < windowCapacity) + notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) + signalNotEmpty(); + return true; + } + + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + boolean offered = false; + int c = windowElementCount.get(); + if (c == windowCapacity) return offered; + Node node = new Node<>(e); + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { + if (windowElementCount.get() < windowCapacity) { + enqueue(node); + offered = true; + c = windowElementCount.incrementAndGet(); + border = node; + if (c < windowCapacity) notFull.signal(); + } + } finally { + // log.debug("1、添加元素:总数:{}、窗口区:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + putLock.unlock(); + } + if (c > 0) signalNotEmpty(); + return offered; + } + + public boolean offerStandby(E e) { + if (e == null) throw new NullPointerException(); + boolean offered = false; + int c = totalElementCount.get(); + if (c == totalCapacity) return offered; + Node node = new Node<>(e); + putLock.lock(); + try { + if (totalElementCount.get() < totalCapacity) { + enqueue(node); + offered = true; + c = windowElementCount.get(); + if (c + 1 <= windowCapacity) { + border = node; + windowElementCount.incrementAndGet(); + } else { + standbyElementCount.incrementAndGet(); + } + if (c + 1 < windowCapacity) notFull.signal(); + } + c = windowElementCount.get(); + } finally { + log.debug("3、添加元素:总数:{}、窗口区:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + putLock.unlock(); + } + if (c != 0) signalNotEmpty(); + return offered; + } + + public E take() throws InterruptedException { + E x; + int c = -1; + takeLock.lockInterruptibly(); + try { + while (windowElementCount.get() == 0) { + notEmpty.await(); + } + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } finally { + log.debug("1、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E x = null; + int c = -1; + long nanos = unit.toNanos(timeout); + takeLock.lockInterruptibly(); + try { + while (windowElementCount.get() == 0) { + if (nanos <= 0) return null; + nanos = notEmpty.awaitNanos(nanos); + } + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } finally { + // log.debug("2、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E poll() { + if (windowElementCount.get() == 0) return null; + E x = null; + int c = -1; + takeLock.lock(); + try { + if (windowElementCount.get() > 0) { + x = dequeue(); + if (border.next == null) { + c = windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + c = windowElementCount.get(); + } + if (c > 0) notEmpty.signal(); + } + } finally { + log.debug("3、提取元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + takeLock.unlock(); + } + if (c != windowCapacity) signalNotFull(); + return x; + } + + public E peek() { + if (windowElementCount.get() == 0) + return null; + takeLock.lock(); + try { + Node first = head.next; + if (first == null) + return null; + else + return first.item; + } finally { + takeLock.unlock(); + } + } + + void unlink(Node p, Node trail, boolean overBorder) { + totalElementCount.getAndDecrement(); + if (p == border || !overBorder) { + if (border.next == null) { + windowElementCount.getAndDecrement(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + } + } else { + standbyElementCount.decrementAndGet(); + } + p.item = null; + trail.next = p.next; + if (last == p) last = trail; + + if (windowElementCount.get() == windowCapacity) notFull.signal(); + } + + public boolean remove(Object o) { + if (o == null) return false; + fullyLock(); + try { + boolean overBorder = false; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (!overBorder) overBorder = trail == border; + if (o.equals(p.item)) { + unlink(p, trail, overBorder); + return true; + } + } + return false; + } finally { + log.debug("1、删除元素:总数:{}、窗口:{}、备份区:{}", totalElementCount.get(), windowElementCount.get(), standbyElementCount.get()); + fullyUnlock(); + } + } + + public boolean contains(Object o) { + if (o == null) return false; + fullyLock(); + try { + for (Node p = head.next; p != null; p = p.next) + if (o.equals(p.item)) + return true; + return false; + } finally { + fullyUnlock(); + } + } + + public Object[] toArray() { + fullyLock(); + try { + int size = windowElementCount.get(); + Object[] a = new Object[size]; + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = p.item; + return a; + } finally { + fullyUnlock(); + } + } + + public T[] toArray(T[] a) { + fullyLock(); + try { + int size = windowElementCount.get(); + if (a.length < size) + a = (T[]) java.lang.reflect.Array.newInstance + (a.getClass().getComponentType(), size); + + int k = 0; + for (Node p = head.next; p != null; p = p.next) + a[k++] = (T) p.item; + if (a.length > k) + a[k] = null; + return a; + } finally { + fullyUnlock(); + } + } + + public String toString() { + fullyLock(); + try { + Node p = head.next; + if (p == null) + return "[]"; + + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (; ; ) { + E e = p.item; + sb.append(e == this ? "(this Collection)" : e); + p = p.next; + if (p == null) + return sb.append(']').toString(); + sb.append(',').append(' '); + } + } finally { + fullyUnlock(); + } + } + + public void clear() { + fullyLock(); + try { + for (Node p, h = head; (p = h.next) != null; h = p) { + h.next = h; + p.item = null; + } + head = last; + totalElementCount.getAndSet(0); + standbyElementCount.getAndSet(0); + if (windowElementCount.getAndSet(0) == windowCapacity) notFull.signal(); + } finally { + fullyUnlock(); + } + } + + public int drainTo(Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + public int drainTo(Collection c, int maxElements) { + if (c == null) + throw new NullPointerException(); + if (c == this) + throw new IllegalArgumentException(); + if (maxElements <= 0) + return 0; + boolean signalNotFull = false; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + int n = Math.min(maxElements, windowElementCount.get()); + + Node h = head; + int i = 0; + try { + while (i < n) { + Node p = h.next; + c.add(p.item); + p.item = null; + h.next = h; + h = p; + ++i; + + if (border.next == null) { + windowElementCount.decrementAndGet(); + } else { + border = border.next; + standbyElementCount.decrementAndGet(); + } + } + return n; + } finally { + if (i > 0) { + head = h; + totalElementCount.getAndAdd(-i); + signalNotFull = (windowElementCount.get() == windowCapacity); + } + } + } finally { + takeLock.unlock(); + if (signalNotFull) + signalNotFull(); + } + } + + public Iterator iterator() { + return new Itr(); + } + + public Spliterator spliterator() { + return new LBQSpliterator(this); + } + + static class Node { + E item; + + /** + * One of: + * - the real successor Node + * - this Node, meaning the successor is head.next + * - null, meaning there is no successor (this is the last node) + */ + Node next; + + Node(E x) { + item = x; + } + } + + static final class LBQSpliterator implements Spliterator { + static final int MAX_BATCH = 1 << 25; + final WindowBlockingQueue queue; + Node current; + int batch; + boolean exhausted; + long est; + + LBQSpliterator(WindowBlockingQueue queue) { + this.queue = queue; + this.est = queue.size(); + } + + public long estimateSize() { + return est; + } + + public Spliterator trySplit() { + Node h; + final WindowBlockingQueue q = this.queue; + int b = batch; + int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1; + if (!exhausted && + ((h = current) != null || (h = q.head.next) != null) && + h.next != null) { + Object[] a = new Object[n]; + int i = 0; + Node p = current; + q.fullyLock(); + try { + if (p != null || (p = q.head.next) != null) { + do { + if ((a[i] = p.item) != null) + ++i; + } while ((p = p.next) != null && i < n); + } + } finally { + q.fullyUnlock(); + } + if ((current = p) == null) { + est = 0L; + exhausted = true; + } else if ((est -= i) < 0L) + est = 0L; + if (i > 0) { + batch = i; + return Spliterators.spliterator + (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT); + } + } + return null; + } + + public void forEachRemaining(Consumer action) { + if (action == null) throw new NullPointerException(); + final WindowBlockingQueue q = this.queue; + if (!exhausted) { + exhausted = true; + Node p = current; + do { + E e = null; + q.fullyLock(); + try { + if (p == null) + p = q.head.next; + while (p != null) { + e = p.item; + p = p.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (e != null) + action.accept(e); + } while (p != null); + } + } + + public boolean tryAdvance(Consumer action) { + if (action == null) throw new NullPointerException(); + final WindowBlockingQueue q = this.queue; + if (!exhausted) { + E e = null; + q.fullyLock(); + try { + if (current == null) + current = q.head.next; + while (current != null) { + e = current.item; + current = current.next; + if (e != null) + break; + } + } finally { + q.fullyUnlock(); + } + if (current == null) + exhausted = true; + if (e != null) { + action.accept(e); + return true; + } + } + return false; + } + + public int characteristics() { + return Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT; + } + } + + private class Itr implements Iterator { + private Node current; + private Node lastRet; + private E currentElement; + + Itr() { + fullyLock(); + try { + current = head.next; + if (current != null) + currentElement = current.item; + } finally { + fullyUnlock(); + } + } + + public boolean hasNext() { + return current != null; + } + + /** + * Returns the next live successor of p, or null if no such. + *

+ * Unlike other traversal methods, iterators need to handle both: + * - dequeued nodes (p.next == p) + * - (possibly multiple) interior removed nodes (p.item == null) + */ + private Node nextNode(Node p) { + for (; ; ) { + Node s = p.next; + if (s == p) + return head.next; + if (s == null || s.item != null) + return s; + p = s; + } + } + + public E next() { + fullyLock(); + try { + if (current == null) + throw new NoSuchElementException(); + E x = currentElement; + lastRet = current; + current = nextNode(current); + currentElement = (current == null) ? null : current.item; + return x; + } finally { + fullyUnlock(); + } + } + + public void remove() { + if (lastRet == null) + throw new IllegalStateException(); + fullyLock(); + try { + Node node = lastRet; + lastRet = null; + boolean overBorder = false; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (!overBorder) overBorder = trail == border; + if (p == node) { + unlink(p, trail, overBorder); + break; + } + } + } finally { + fullyUnlock(); + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java new file mode 100644 index 0000000..547ab68 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/Tree.java @@ -0,0 +1,137 @@ +package com.njzscloud.common.core.tree; + + +import cn.hutool.core.collection.CollUtil; +import com.njzscloud.common.core.utils.GroupUtil; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 树型结构工具 + */ +public class Tree { + + /** + * 创建树形数据(倒序) + * + * @param src 源集合 + * @param rootId 根节点 ID + * @param 节点 ID 类型 + * @return List<? extends TreeNode<T>> + */ + public static List> listToTreeDesc(List> src, T rootId) { + return listToTree(src.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList()), TreeNode::getId, TreeNode::getPid, TreeNode::setChildren, rootId, true); + } + + /** + * 创建树形数据(正序) + * + * @param src 源集合 + * @param rootId 根节点 ID + * @param 节点 ID 类型 + * @return List<? extends TreeNode<T>> + */ + public static List> listToTreeAsc(List> src, T rootId) { + return listToTree(src.stream().sorted().collect(Collectors.toList()), TreeNode::getId, TreeNode::getPid, TreeNode::setChildren, rootId, false); + } + + /** + * 创建树形数据(排序) + * + * @param src 源集合 + * @param idFn id 提供函数 + * @param pidFn pid 提供函数 + * @param setChildrenFn 子节点设置函数 + * @param rootId 根节点 ID + * @param reverse 是否倒序 + * @param 源集合元素类型 + * @param 节点 ID 类型 + * @return List<M> + */ + public static , T> List listToTree(List src, Function idFn, Function pidFn, BiConsumer> setChildrenFn, T rootId, boolean reverse) { + if (CollUtil.isEmpty(src)) return Collections.emptyList(); + if (reverse) { + src = src + .stream() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + + Map> pid_treeNode_map = GroupUtil.k_a(src, pidFn); + + for (M m : src) { + setChildrenFn.accept(m, pid_treeNode_map.get(idFn.apply(m))); + } + return CollUtil.emptyIfNull(pid_treeNode_map.get(rootId)); + } + + /** + * 创建树形数据(不排序) + * + * @param src 源集合 + * @param idFn id 提供函数 + * @param pidFn pid 提供函数 + * @param setChildrenFn 子节点设置函数 + * @param rootId 根节点 ID + * @param 源集合元素类型 + * @param 节点 ID 类型 + * @return List<M> + */ + public static List listToTree(List src, Function idFn, Function pidFn, BiConsumer> setChildrenFn, T rootId) { + if (CollUtil.isEmpty(src)) return Collections.emptyList(); + + Map> pid_treeNode_map = GroupUtil.k_a(src, pidFn); + + for (M m : src) { + setChildrenFn.accept(m, pid_treeNode_map.get(idFn.apply(m))); + } + return CollUtil.emptyIfNull(pid_treeNode_map.get(rootId)); + } + + /** + * 扁平化树 + * + * @param src 源集合 树形结构 + * @param getChildrenFn 子节点获取函数 + * @param convert 转换函数, 源集合 元素 -> 结果集合 元素 + * @param 源集合 元素类型 + * @param 结果集合 元素类型 + * @return List<D> + */ + public static List treeToList(List src, Function> getChildrenFn, Function convert) { + List dest; + if (CollUtil.isNotEmpty(src)) { + dest = new ArrayList<>(); + treeToList(src, dest, getChildrenFn, convert); + } else { + dest = Collections.emptyList(); + } + return dest; + } + + /** + * 扁平化树 + * + * @param src 源集合 树形结构 + * @param dest 结果集合 + * @param getChildrenFn 子节点获取函数 + * @param convert 转换函数, 源集合 元素 -> 结果集合 元素 + * @param 源集合 元素类型 + * @param 结果集合 元素类型 + */ + public static void treeToList(List src, List dest, Function> getChildrenFn, Function convert) { + if (CollUtil.isNotEmpty(src)) { + for (S s : src) { + D d = convert.apply(s); + dest.add(d); + List children = getChildrenFn.apply(s); + if (CollUtil.isNotEmpty(children)) { + Tree.treeToList(children, dest, getChildrenFn, convert); + } + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java new file mode 100644 index 0000000..18faf02 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tree/TreeNode.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.core.tree; + + +import java.util.List; + +/** + * 树结点 + * + * @param Id 的数据类型 + */ +public interface TreeNode extends Comparable> { + /** + * 节点 ID + * + * @return T id + */ + T getId(); + + /** + * 节点 上级 ID + * + * @return T pid + */ + T getPid(); + + /** + * 排序 + * + * @return int + */ + int getSort(); + + /** + * 子节点 + * + * @return List<? extends TreeNode<T>> + */ + List> getChildren(); + + /** + * 设置子节点 + * + * @param children 子节点 + */ + void setChildren(List> children); + + @Override + default int compareTo(TreeNode o) { + return this.getSort() - o.getSort(); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java new file mode 100644 index 0000000..c8ecd7e --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple2.java @@ -0,0 +1,118 @@ +package com.njzscloud.common.core.tuple; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 2 元组 + * + * @param <_0> + * @param <_1> + */ +public class Tuple2<_0, _1> { + + /** + * 元组大小 + */ + @JsonIgnore + public final int size = 2; + /** + * 第 0 个数据 + */ + @JsonIgnore + protected final _0 _0_; + /** + * 第 1 个数据 + */ + @JsonIgnore + protected final _1 _1_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + */ + public Tuple2(_0 _0_, _1 _1_) { + this._0_ = _0_; + this._1_ = _1_; + } + + /** + * 创建 Tuple2 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @return Tuple2<__0, __1> + */ + public static <__0, __1> Tuple2<__0, __1> of(__0 _0_, __1 _1_) { + return new Tuple2<>(_0_, _1_); + } + + /** + * 获取第 0 个数据 + */ + @JsonProperty("0") + public _0 get_0() { + return _0_; + } + + /** + * 获取第 1 个数据 + */ + @JsonProperty("1") + public _1 get_1() { + return _1_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @SuppressWarnings("unchecked") + public T get(int index) { + return switch (index) { + case 0 -> (T) this._0_; + case 1 -> (T) this._1_; + default -> null; + }; + } + + /** + * 转换成 List + * + * @return List<Object> + */ + public List toList() { + return Arrays.asList(this.toArray()); + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + public Object[] toArray() { + return new Object[]{this._0_, this._1_}; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tuple2 tuple2 = (Tuple2) o; + return Objects.equals(_0_, tuple2._0_) && Objects.equals(_1_, tuple2._1_); + } + + @Override + public int hashCode() { + return Objects.hash(_0_, _1_); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java new file mode 100644 index 0000000..0495c6d --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple3.java @@ -0,0 +1,86 @@ +package com.njzscloud.common.core.tuple; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 3 元组 + * + * @param <_0> + * @param <_1> + * @param <_2> + */ +public class Tuple3<_0, _1, _2> extends Tuple2<_0, _1> { + + /** + * 元组大小 + */ + @JsonIgnore + public final int size = 3; + + /** + * 第 2 个数据 + */ + @JsonIgnore + protected final _2 _2_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + */ + public Tuple3(_0 _0_, _1 _1_, _2 _2_) { + super(_0_, _1_); + this._2_ = _2_; + } + + /** + * 创建 Tuple3 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @return Tuple3<__0, __1, __2> + */ + public static <__0, __1, __2> Tuple3<__0, __1, __2> of(__0 _0_, __1 _1_, __2 _2_) { + return new Tuple3<>(_0_, _1_, _2_); + } + + /** + * 获取第 2 个数据 + */ + @JsonProperty("2") + public _2 get_2() { + return _2_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @Override + @SuppressWarnings("unchecked") + public T get(int index) { + return switch (index) { + case 0 -> (T) this._0_; + case 1 -> (T) this._1_; + case 2 -> (T) this._2_; + default -> null; + }; + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + @Override + public Object[] toArray() { + return new Object[]{this._0_, this._1_, this._2_}; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java new file mode 100644 index 0000000..ec075ff --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/tuple/Tuple4.java @@ -0,0 +1,88 @@ +package com.njzscloud.common.core.tuple; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 4 元组 + * + * @param <_0> + * @param <_1> + * @param <_2> + * @param <_3> + */ +public class Tuple4<_0, _1, _2, _3> extends Tuple3<_0, _1, _2> { + /** + * 元组大小 + */ + @JsonIgnore + public final int size = 4; + + /** + * 第 3 个数据 + */ + protected final _3 _3_; + + /** + * 构造方法 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @param _3_ 第 3 个数据 + */ + public Tuple4(_0 _0_, _1 _1_, _2 _2_, _3 _3_) { + super(_0_, _1_, _2_); + this._3_ = _3_; + } + + /** + * 创建 Tuple4 + * + * @param _0_ 第 0 个数据 + * @param _1_ 第 1 个数据 + * @param _2_ 第 2 个数据 + * @param _3_ 第 3 个数据 + * @return Tuple3<__0, __1, __2, __3> + */ + public static <__0, __1, __2, __3> Tuple4<__0, __1, __2, __3> of(__0 _0_, __1 _1_, __2 _2_, __3 _3_) { + return new Tuple4<>(_0_, _1_, _2_, _3_); + } + + /** + * 获取第 3 个数据 + */ + @JsonProperty("3") + public _3 get_3() { + return _3_; + } + + /** + *

按索引获取数据

+ *

索引越界将返回 null

+ * + * @param index 索引 + * @return T + */ + @Override + @SuppressWarnings("unchecked") + public T get(int index) { + return switch (index) { + case 0 -> (T) this._0_; + case 1 -> (T) this._1_; + case 2 -> (T) this._2_; + case 3 -> (T) this._3_; + default -> null; + }; + } + + /** + * 转换成 数组 + * + * @return Object[] + */ + @Override + public Object[] toArray() { + return new Object[]{this._0_, this._1_, this._2_, this._3_}; + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/BCD.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/BCD.java new file mode 100644 index 0000000..cc7913f --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/BCD.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.core.utils; + +public class BCD { + public static String bcdToStr(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + sb.append(String.format("%02x", b)); + } + // 移除前面的0 + return sb.toString().replaceFirst("^0+", ""); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CallerUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CallerUtil.java new file mode 100644 index 0000000..9e93aff --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CallerUtil.java @@ -0,0 +1,80 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.tuple.Tuple2; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +public class CallerUtil { + + /** + * 获取方法调用者的详细信息(类名、方法名、行号) + * + * @return 调用者信息封装类,若获取失败返回 null + */ + public static Caller getCaller(int depth) { + /* + getStackTrace + getCaller + getCaller 的调用者 + getCaller 的调用者的调用者 + */ + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + + int i = 2 + depth; + if (stackTrace.length < i + 1) { + // 调用栈过短(理论上不可能,除非在 JVM 启动初期调用) + return Caller.defaultCaller; + } + + return resolveCaller(stackTrace, depth); + } + + public static Caller resolveCaller(StackTraceElement[] stackTrace, int depth) { + StackTraceElement callerElement = stackTrace[depth]; + // 解析栈元素的信息 + String className = callerElement.getClassName(); // 调用者类名(全限定名) + String methodName = callerElement.getMethodName();// 调用者方法名 + int lineNumber = callerElement.getLineNumber(); // 调用者代码行号(-1 表示未知) + String fileName = callerElement.getFileName(); // 调用者所在文件名(可能为 null) + return new Caller(className, methodName, lineNumber, fileName); + } + + public static Tuple2 printStackTrace(Throwable throwable) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (PrintStream printStream = new PrintStream(outputStream)) { + // 3. 调用 printStackTrace,输出到 PrintStream(实际写入 ByteArrayOutputStream) + throwable.printStackTrace(printStream); + // 4. 将字节流转为字符串(默认 UTF-8 编码,兼容所有字符) + String string = outputStream.toString(StandardCharsets.UTF_8); + return Tuple2.of(string, resolveCaller(throwable.getStackTrace(), 0)); + } catch (Exception e) { + // 极端情况下(如编码异常),返回简化信息 + return Tuple2.of(throwable.getMessage(), resolveCaller(throwable.getStackTrace(), 0)); + } + } + + /** + * 调用者信息封装类(用于结构化返回结果) + * + * @param className Getter 方法 全限定类名(如:com.example.Test) + * @param methodName 方法名(如:main) + * @param lineNumber 调用行号 + * @param fileName 文件名(如:Test.java) + */ + public record Caller(String className, String methodName, int lineNumber, String fileName) { + public static Caller defaultCaller = new Caller(); + + public Caller() { + this("", "", -1, ""); + } + + @Override + public String toString() { + return StrUtil.format("调用者信息, 类名:{}、方法名:{}、行号:{}、文件名:{}", + className, methodName, lineNumber, fileName); + } + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CoordinateConverter.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CoordinateConverter.java new file mode 100644 index 0000000..73b3647 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/CoordinateConverter.java @@ -0,0 +1,104 @@ +package com.njzscloud.common.core.utils; + +import com.njzscloud.common.core.tuple.Tuple2; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 坐标转换工具类:WGS-84转GCJ-02(火星坐标系) + * WGS-84:GPS设备获取的原始坐标 + * GCJ-02:高德地图、谷歌中国地图等使用的坐标系 + */ +public class CoordinateConverter { + // 长半径 + private static final double PI = 3.1415926535897932384626; + private static final double A = 6378245.0; + // 扁率 + private static final double EE = 0.00669342162296594323; + + /** + * 判断坐标是否在中国境内(不在中国境内则不做偏移) + * + * @param lat 纬度 + * @param lon 经度 + * @return 是否在中国境内 + */ + public static boolean outOfChina(double lat, double lon) { + if (lon < 72.004 || lon > 137.8347) { + return true; + } + return lat < 0.8293 || lat > 55.8271; + } + + /** + * 纬度转换 + * + * @param x 经度 + * @param y 纬度 + * @return 转换后的纬度偏移量 + */ + private static double transformLat(double x, double y) { + double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + /** + * 经度转换 + * + * @param x 经度 + * @param y 纬度 + * @return 转换后的经度偏移量 + */ + private static double transformLon(double x, double y) { + double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * 将WGS-84坐标转换为GCJ-02坐标 + * + * @param wgsLat WGS-84纬度 + * @param wgsLon WGS-84经度 + * @return 转换后的坐标数组 [纬度, 经度] + */ + public static Tuple2 wgs84ToGcj02(double wgsLat, double wgsLon) { + // 如果不在中国境内,直接返回原坐标 + if (outOfChina(wgsLat, wgsLon)) { + return Tuple2.of(wgsLat, wgsLon); + } + + double dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0); + double dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0); + double radLat = wgsLat / 180.0 * PI; + double magic = Math.sin(radLat); + magic = 1 - EE * magic * magic; + double sqrtMagic = Math.sqrt(magic); + dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); + dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); + + // 保留6位小数,约10厘米精度 + double gcjLat = round(wgsLat + dLat, 6); + double gcjLon = round(wgsLon + dLon, 6); + + return Tuple2.of(gcjLat, gcjLon); + } + + /** + * 四舍五入保留指定小数位数 + * + * @param value 数值 + * @param scale 小数位数 + * @return 处理后的数值 + */ + private static double round(double value, int scale) { + return new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP).doubleValue(); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java new file mode 100644 index 0000000..3dc18ad --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Globs.java @@ -0,0 +1,195 @@ +package com.njzscloud.common.core.utils; + +import java.util.regex.PatternSyntaxException; + +/** + * @see sun.nio.fs.Globs + */ +public class Globs { + private static final String regexMetaChars = ".^$+{[]|()"; + private static final String globMetaChars = "\\*?[{"; + private static final char EOL = 0; // TBD + + private Globs() { + } + + private static boolean isRegexMeta(char c) { + return regexMetaChars.indexOf(c) != -1; + } + + private static boolean isGlobMeta(char c) { + return globMetaChars.indexOf(c) != -1; + } + + private static char next(String glob, int i) { + if (i < glob.length()) { + return glob.charAt(i); + } + return EOL; + } + + /** + * Creates a regex pattern from the given glob expression. + * + * @throws PatternSyntaxException + */ + private static String toRegexPattern(String globPattern, boolean isDos) { + boolean inGroup = false; + StringBuilder regex = new StringBuilder("^"); + + int i = 0; + while (i < globPattern.length()) { + char c = globPattern.charAt(i++); + switch (c) { + case '\\': + // escape special characters + if (i == globPattern.length()) { + throw new PatternSyntaxException("No character to escape", globPattern, i - 1); + } + char next = globPattern.charAt(i++); + if (isGlobMeta(next) || isRegexMeta(next)) { + regex.append('\\'); + } + regex.append(next); + break; + case '/': + if (isDos) { + regex.append("\\\\"); + } else { + regex.append(c); + } + break; + case '[': + // don't match name separator in class + if (isDos) { + regex.append("[[^\\\\]&&["); + } else { + regex.append("[[^/]&&["); + } + if (next(globPattern, i) == '^') { + // escape the regex negation char if it appears + regex.append("\\^"); + i++; + } else { + // negation + if (next(globPattern, i) == '!') { + regex.append('^'); + i++; + } + // hyphen allowed at start + if (next(globPattern, i) == '-') { + regex.append('-'); + i++; + } + } + boolean hasRangeStart = false; + char last = 0; + while (i < globPattern.length()) { + c = globPattern.charAt(i++); + if (c == ']') { + break; + } + if (c == '/' || (isDos && c == '\\')) { + throw new PatternSyntaxException("Explicit 'name separator' in class", + globPattern, i - 1); + } + // TBD: how to specify ']' in a class? + if (c == '\\' || c == '[' || + c == '&' && next(globPattern, i) == '&') { + // escape '\', '[' or "&&" for regex class + regex.append('\\'); + } + regex.append(c); + + if (c == '-') { + if (!hasRangeStart) { + throw new PatternSyntaxException("Invalid range", + globPattern, i - 1); + } + if ((c = next(globPattern, i++)) == EOL || c == ']') { + break; + } + if (c < last) { + throw new PatternSyntaxException("Invalid range", + globPattern, i - 3); + } + regex.append(c); + hasRangeStart = false; + } else { + hasRangeStart = true; + last = c; + } + } + if (c != ']') { + throw new PatternSyntaxException("Missing ']", globPattern, i - 1); + } + regex.append("]]"); + break; + case '{': + if (inGroup) { + throw new PatternSyntaxException("Cannot nest groups", + globPattern, i - 1); + } + regex.append("(?:(?:"); + inGroup = true; + break; + case '}': + if (inGroup) { + regex.append("))"); + inGroup = false; + } else { + regex.append('}'); + } + break; + case ',': + if (inGroup) { + regex.append(")|(?:"); + } else { + regex.append(','); + } + break; + case '*': + if (next(globPattern, i) == '*') { + // crosses directory boundaries + regex.append(".*"); + i++; + } else { + // within directory boundary + if (isDos) { + regex.append("[^\\\\]*"); + } else { + regex.append("[^/]*"); + } + } + break; + case '?': + if (isDos) { + regex.append("[^\\\\]"); + } else { + regex.append("[^/]"); + } + break; + + default: + if (isRegexMeta(c)) { + regex.append('\\'); + } + regex.append(c); + } + } + + if (inGroup) { + throw new PatternSyntaxException("Missing '}", globPattern, i - 1); + } + + return regex.append('$').toString(); + } + + static String toUnixRegexPattern(String globPattern) { + return toRegexPattern(globPattern, false); + } + + static String toWindowsRegexPattern(String globPattern) { + return toRegexPattern(globPattern, true); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java new file mode 100644 index 0000000..d2c8273 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/GroupUtil.java @@ -0,0 +1,625 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 对象分组工具 + */ +public class GroupUtil { + + // region K-O + + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 值类型 / 源集合元素类型 + * @param 键类型 + * @return Map<K, SV> + */ + public static Map k_o(Collection src, Function kf) { + return k_o(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, Function kf, Function vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 值类型 / 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Collection src, Function kf, Supplier mf) { + return k_o(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + return k_o(src.stream(), kf, vf, mf); + } + + // endregion + + // region Collection 索引 + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, VT> + */ + public static Map k_o(Collection src, BiFunction kf) { + return k_o(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param vf V 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, BiFunction kf, Function vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Collection src, BiFunction kf, Supplier mf) { + return k_o(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ *

kf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf K 提供函数 + * @param vf V 提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, BiFunction kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(i, sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(sv); + map.put(k, v); + i++; + } + return map; + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, Function kf, BiFunction vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ * Map 为 mf, 键为 kf, 值为 vf

+ *

vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, Function kf, BiFunction vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(i, sv); + map.put(k, v); + i++; + } + return map; + } + + /** + *

分组

+ * Map 为 HashMap, 键为 kf, 值为 vf

+ *

kf vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Collection src, BiFunction kf, BiFunction vf) { + return k_o(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ * Map 为 mf, 键为 kf, 值为 vf

+ *

kf vf 中可得到 源集合元素 索引

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Collection src, BiFunction kf, BiFunction vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + + M map = mf.get(); + + int i = 0; + for (SV sv : src) { + K k = kf.apply(i, sv); + if (map.containsKey(k)) throw Exceptions.error("重复键:{}", sv); + V v = vf.apply(i, sv); + map.put(k, v); + i++; + } + return map; + } + // endregion + + // region Stream + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, SV> + */ + public static Map k_o(Stream stream, Function kf) { + return k_o(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, V> + */ + public static Map k_o(Stream stream, Function kf, Function vf) { + return k_o(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, SV> + */ + public static > M k_o(Stream stream, Function kf, Supplier mf) { + return k_o(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, V> + */ + public static > M k_o(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + BinaryOperator op = (oldVal, val) -> { + throw Exceptions.error("值【{}】与【{}】对应的键重复", Jackson.toJsonStr(oldVal), Jackson.toJsonStr(val)); + }; + return stream.collect(Collectors.toMap(kf, vf, op, mf)); + } + // endregion + + // endregion + + // region K-A + + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Collection src, Function kf) { + return k_a(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Collection src, Function kf, Function vf) { + return k_a(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, List<SV>> + */ + public static >> M k_a(Collection src, Function kf, Supplier mf) { + return k_a(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, List<V>> + */ + public static >> M k_a(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + return src.stream().collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toList()))); + } + // endregion + + // region Stream + + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Stream stream, Function kf) { + return k_a(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, List<V>> + */ + public static Map> k_a(Stream stream, Function kf, Function vf) { + return k_a(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param Map 类型 + * @return Map<K, List<SV>> + */ + public static >> M k_a(Stream stream, Function kf, Supplier mf) { + return k_a(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, List<V>> + */ + public static >> M k_a(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + return stream.collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toList()))); + } + // endregion + + // endregion + + // region K-S + // region Collection + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, Set<SV>> + */ + public static Map> k_s(Collection src, Function kf) { + return k_s(src, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, Set<V>> + */ + public static Map> k_s(Collection src, Function kf, Function vf) { + return k_s(src, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @param Map 类型 + * @return Map<K, Set<SV>> + */ + public static >> M k_s(Collection src, Function kf, Supplier mf) { + return k_s(src, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param src 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, Set<V>> + */ + public static >> M k_s(Collection src, Function kf, Function vf, Supplier mf) { + if (CollUtil.isEmpty(src)) return MapUtil.empty(null); + return src.stream().collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toSet()))); + } + // endregion + + // region Stream + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 源集合元素

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @return Map<K, Set<SV>> + */ + public static Map> k_s(Stream stream, Function kf) { + return k_s(stream, kf, it -> it, HashMap::new); + } + + /** + *

分组

+ *

Map 为 HashMap, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @return Map<K, Set<V>> + */ + public static Map> k_s(Stream stream, Function kf, Function vf) { + return k_s(stream, kf, vf, HashMap::new); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param mf Map 提供函数 + * @param 键类型 + * @param 源集合元素类型 + * @param Map 类型 + * @return Map<K, Set<SV>> + */ + public static >> M k_s(Stream stream, Function kf, Supplier mf) { + return k_s(stream, kf, it -> it, mf); + } + + /** + *

分组

+ *

Map 为 mf, 键为 kf, 值为 vf

+ * + * @param stream 源集合 + * @param kf 键提供函数 + * @param vf 值提供函数 + * @param mf Map 提供函数 + * @param 源集合元素类型 + * @param 键类型 + * @param 值类型 + * @param Map 类型 + * @return Map<K, Set<V>> + */ + public static >> M k_s(Stream stream, Function kf, Function vf, Supplier mf) { + if (stream == null) return MapUtil.empty(null); + return stream.collect(Collectors.groupingBy(kf, mf, Collectors.mapping(vf, Collectors.toSet()))); + } + // endregion + + // endregion + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/ImgUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/ImgUtil.java new file mode 100644 index 0000000..51ca441 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/ImgUtil.java @@ -0,0 +1,164 @@ +package com.njzscloud.common.core.utils; + +import com.njzscloud.common.core.ex.Exceptions; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class ImgUtil { + + // 默认字体(可根据需求调整) + private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 24); + // 默认水印颜色(半透明黑色) + private static final Color DEFAULT_COLOR = Color.WHITE; + // 行间距(字体大小的1.2倍) + private static final float LINE_SPACING_RATIO = 1.2f; + // 左右边距(避免文本贴边) + private static final int PADDING_HORIZONTAL = 10; + // 上下边距 + private static final int PADDING_VERTICAL = 10; + + /** + * 在左上角添加文字水印 + * + * @param sourceImageIn 图片 + * @param type jpg 或 png + * @param txt 文字 + */ + public static byte[] addTxtWatermark(InputStream sourceImageIn, String type, String txt) { + try { + BufferedImage sourceImage = ImageIO.read(sourceImageIn); + if (sourceImage == null) { + throw Exceptions.error("图片读取失败"); + } + + BufferedImage targetImage = new BufferedImage( + sourceImage.getWidth(), + sourceImage.getHeight(), + type.equalsIgnoreCase("png") ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB + ); + Graphics2D g2d = targetImage.createGraphics(); + + g2d.drawImage(sourceImage, 0, 0, null); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g2d.setFont(DEFAULT_FONT); + g2d.setColor(DEFAULT_COLOR); + + int maxWatermarkWidth = (sourceImage.getWidth() / 2) - 2 * PADDING_HORIZONTAL; + if (maxWatermarkWidth <= 0) { + throw Exceptions.error("图片宽度过小,无法添加水印"); + } + int lineHeight = g2d.getFontMetrics().getHeight(); + + String[] txts = txt.split("\n"); + int currentY = PADDING_VERTICAL + g2d.getFontMetrics().getAscent(); + int maxHeight = targetImage.getHeight(); + + for (String s : txts) { + List lines = wrapText(s, DEFAULT_FONT, maxWatermarkWidth); + for (String line : lines) { + g2d.drawString(line, PADDING_HORIZONTAL, currentY); + currentY += (int) (lineHeight * LINE_SPACING_RATIO); + if (currentY >= maxHeight) { + throw Exceptions.error("图片高度不足,无法添加水印"); + } + } + } + + g2d.dispose(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + boolean write = ImageIO.write(targetImage, type, outputStream); + if (write) { + return outputStream.toByteArray(); + } else { + return new byte[0]; + } + } catch (Exception e) { + throw Exceptions.error(e, "水印添加失败"); + } + } + + /** + * 文本自动换行处理 + * + * @param text 待换行文本 + * @param font 字体 + * @param maxWidth 最大宽度(像素) + * @return 换行后的文本行列表 + */ + private static List wrapText(String text, Font font, int maxWidth) { + List lines = new ArrayList<>(); + if (text == null || text.isBlank()) { + return lines; + } + + Graphics2D g2d = (Graphics2D) new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics(); + g2d.setFont(font); + FontMetrics metrics = g2d.getFontMetrics(); + g2d.dispose(); + + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String tempLine = currentLine.isEmpty() ? word : currentLine + " " + word; + Rectangle2D bounds = metrics.getStringBounds(tempLine, null); + + if (bounds.getWidth() <= maxWidth) { + // 当前行加上单词后未超过最大宽度,继续添加 + currentLine = new StringBuilder(tempLine); + } else { + // 超过最大宽度,保存当前行,开始新行 + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(word); + } else { + // 单个单词超过最大宽度,强制截断(特殊情况处理) + lines.add(truncateWord(word, metrics, maxWidth)); + currentLine = new StringBuilder(); + } + } + } + + // 添加最后一行 + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + } + + return lines; + } + + /** + * 单个单词超长时的强制截断(按字符分割) + * + * @param word 超长单词 + * @param metrics 字体度量 + * @param maxWidth 最大宽度 + * @return 截断后的单词片段 + */ + private static String truncateWord(String word, FontMetrics metrics, int maxWidth) { + StringBuilder truncated = new StringBuilder(); + for (char c : word.toCharArray()) { + String temp = truncated + String.valueOf(c); + if (metrics.getStringBounds(temp, null).getWidth() <= maxWidth) { + truncated.append(c); + } else { + break; + } + } + return truncated.toString(); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java new file mode 100644 index 0000000..d1ba242 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Key.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.Exceptions; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + *

缓存/Redis 等 Key

+ */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Key { + private final String keyTpl; + + /** + *

创建 KEY

+ *

KEY 字符串模板:可用:{名称} 做为占位符

+ * + * @param keyTpl KEY 字符串模板 + * @return KEY 对象 + */ + public static Key create(String keyTpl) { + Assert.notEmpty(keyTpl, () -> Exceptions.error("KEY 字符串模板不能为空")); + keyTpl = keyTpl.replaceAll("\\{\\w+}", "{}"); + return new Key(keyTpl); + } + + /** + *

填充 KEY 模板

+ * + * @param params 填充参数 + * @return key 值 + */ + public String fill(Object... params) { + int count = StrUtil.count(keyTpl, "{}"); + Assert.isTrue(count == params.length, + () -> Exceptions.error("占位符数量:[{}]与参数数量:[{}]不一致", count, params.length)); + return StrUtil.format(keyTpl, params); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Mime.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Mime.java new file mode 100644 index 0000000..7783779 --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/Mime.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.util.StrUtil; + +/** + * MIME + */ +public class Mime { + public static final String FORM = "application/x-www-form-urlencoded"; + public static final String MULTIPART_FORM = "multipart/form-data"; + public static final String BINARY = "application/octet-stream"; + public static final String JSON = "application/json"; + public static final String X_NDJSON = "application/x-ndjson"; + public static final String XML = "application/xml"; + public static final String TXT = "text/plain"; + public static final String HTML = "text/html"; + public static final String CSS = "text/css"; + public static final String GIF = "image/gif"; + public static final String JPG = "image/jpeg"; + public static final String PNG = "image/png"; + public static final String SVG = "image/svg+xml"; + public static final String WEBP = "image/webp"; + public static final String PDF = "image/pdf"; + public static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + public static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + public static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + public static final String PPT = "application/vnd.ms-powerpoint"; + public static final String XLS = "application/vnd.ms-excel"; + public static final String DOC = "application/msword"; + public static final String ZIP = "application/zip"; + public static final String MP3 = "audio/mpeg"; + public static final String MP4 = "video/mp4"; + + + public static String u8Val(String mime) { + return StrUtil.format("{};charset={}", mime, "UTF-8"); + } + +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java new file mode 100644 index 0000000..bc2b11d --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/R.java @@ -0,0 +1,243 @@ +package com.njzscloud.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * 响应信息主体 + * + * @param + */ +@Getter +@SuppressWarnings("unchecked") +public final class R { + /** + *

错误码 >= 0

+ *

0 --> 没有错误

+ *

其他 --> 有错误

+ */ + private final int code; + /** + * 是否成功 + */ + private final boolean success; + + /** + * 简略信息 + */ + private final String msg; + /** + * 响应数据 + */ + private T data; + /** + * 详细信息 + */ + private Object message; + + private R(T data, ExceptionMsg msg, Object message) { + this(msg.code, data, msg.msg, message); + } + + @JsonCreator + private R(@JsonProperty("code") int code, + @JsonProperty("data") T data, + @JsonProperty("msg") String msg, + @JsonProperty("message") Object message) { + this.data = data; + this.code = code; + this.msg = msg; + this.message = message; + this.success = code == 0; + } + + /** + * 成功,响应码:0,响应信息:成功,详细信息:null,响应数据:null + * + * @param 响应数据的类型 + * @return R<D> + */ + public static R success() { + return new R<>(0, null, "成功", null); + } + + /** + * 成功,响应码:0,响应信息:成功,详细信息:null + * + * @param data 响应数据 + * @param 响应数据的类型 + * @return R<D> + */ + public static R success(D data) { + return new R<>(0, data, "成功", null); + } + + /** + * 成功,响应码:0,详细信息:null + * + * @param data 响应数据 + * @param msg 响应信息 + * @param 响应数据的类型 + * @return R<D> + */ + public static R success(D data, String msg) { + return new R<>(0, data, msg, null); + } + + /** + * 失败,响应码:11111,响应信息:系统异常!,详细信息:null,响应数据:null + * + * @param 响应数据的类型 + * @return R<D> + */ + public static R failed() { + return new R<>(null, ExceptionMsg.SYS_EXP_MSG, null); + } + + /** + * 失败,详细信息:null,响应数据:null + * + * @param msg 简略错误信息 + * @param 响应数据的类型 + * @return R<D> + * @see ExceptionMsg + */ + public static R failed(ExceptionMsg msg) { + return new R<>(null, msg, null); + } + + public static R failed(ExceptionMsg msg, Object message) { + return new R<>(null, msg, message); + } + + public static R failed(ExceptionMsg msg, String message, Object... param) { + if (StrUtil.isNotBlank(message) && param != null && param.length > 0) { + message = StrUtil.format(message, param); + } + return new R<>(null, msg, message); + } + + /** + * 失败,详细信息:null + * + * @param data 响应数据 + * @param msg 简略错误信息 + * @param 响应数据的类型 + * @return R<D> + * @see ExceptionMsg + */ + public static R failed(D data, ExceptionMsg msg) { + return new R<>(null, msg, null); + } + + public static R failed(D data, ExceptionMsg msg, Object message) { + return new R<>(null, msg, message); + } + + /** + * 设置响应数据 + * + * @param data 响应数据 + * @return R<D> + */ + public R setData(T data) { + this.data = data; + return this; + } + + /** + *

添加响应数据

+ *

需确保 data 为 Map 类型且 Key 为 String,data 为 null 时,会新建

+ * + * @param key 键 + * @param val 值 + * @return R<T> + */ + public R> put(String key, V val) { + if (this.data == null) { + R> r = new R<>(this.code, new HashMap<>(), this.msg, this.message); + r.data.put(key, val); + return r; + } else if (Map.class.isAssignableFrom(data.getClass())) { + ((Map) this.data).put(key, val); + return (R>) this; + } + + throw Exceptions.error("响应信息构建失败"); + } + + /** + *

添加响应数据

+ *

需确保 data 为 List 类型,data 为 null 时,会新建

+ * + * @param val 值 + * @return R<T> + */ + public R> add(V val) { + if (this.data == null) { + R> r = new R<>(this.code, new ArrayList<>(), this.msg, this.message); + r.data.add(val); + return r; + } else if (List.class.isAssignableFrom(data.getClass())) { + ((List) this.data).add(val); + return (R>) this; + } + throw Exceptions.error("响应信息构建失败"); + } + + /** + * 设置详细信息 + * + * @param message 详细信息 + * @return R<T> + */ + public R setMessage(Object message) { + this.message = message; + return this; + } + + /** + * 设置详细信息 + * + * @param message 消息字符串模板,占位符:{} + * @param param 占位符参数 + * @return R<T> + */ + public R setMessage(String message, Object... param) { + if (StrUtil.isNotBlank(message) && param != null && param.length > 0) { + message = StrUtil.format(message, param); + } + this.message = message; + return this; + } + + /** + *

转换

+ *

将响应数据转换为其他对象

+ *

code、msg、message 不变化

+ * + * @param converter 转换器 + * @param 新响应数据的类型 + * @return R<D> + */ + public R convert(Function converter) { + D d = converter.apply(this.data); + return new R<>(this.code, d, this.msg, this.message); + } + + @Override + public String toString() { + return Jackson.toJsonStr(this); + } +} diff --git a/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/StrUtil.java b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/StrUtil.java new file mode 100644 index 0000000..79a329a --- /dev/null +++ b/njzscloud-common/njzscloud-common-core/src/main/java/com/njzscloud/common/core/utils/StrUtil.java @@ -0,0 +1,113 @@ +package com.njzscloud.common.core.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StrUtil { + // 匹配非特殊字符的正则表达式,对应 TS 中的 [^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+ + private static final Pattern SPLIT_CHAR_PATTERN = Pattern.compile("[^\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f]+"); + + // 匹配大写字母的正则表达式,用于分割单词 + private static final Pattern UPPER_CASE_PATTERN = Pattern.compile("([A-Z])"); + + /** + * 检查字符串是否为空 + */ + private static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + /** + * 首字母大写 + */ + private static String capitalize(String word) { + if (isBlank(word)) { + return word; + } + return Character.toUpperCase(word.charAt(0)) + + (word.length() > 1 ? word.substring(1).toLowerCase() : ""); + } + + /** + * 以大写字母为分割符,转换成其他分隔符 + */ + public static String processWords(String str, WordJoiner joiner) { + if (isBlank(str)) { + return ""; + } + + // 处理字符串:在大写字母前添加分隔符,然后转为小写 + String processed = UPPER_CASE_PATTERN.matcher(str).replaceAll(" $1").toLowerCase(); + + // 提取匹配的单词 + Matcher matcher = SPLIT_CHAR_PATTERN.matcher(processed); + List words = new ArrayList<>(); + while (matcher.find()) { + words.add(matcher.group().trim()); + } + + // 使用 joiner 连接单词 + StringBuilder result = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + result.append(joiner.join(words.get(i), i)); + } + return result.toString(); + } + + /** + * 小驼峰命名 (camelCase) + */ + public static String camelCase(String str) { + return processWords(str, (word, index) -> { + if (index == 0) { + return word; + } else { + return capitalize(word); + } + }); + } + + /** + * 大驼峰命名 (PascalCase) + */ + public static String pascalCase(String str) { + String camel = camelCase(str); + return capitalize(camel); + } + + /** + * 下划线命名 (snake_case) + */ + public static String snakeCase(String str) { + return processWords(str, (word, index) -> { + if (index == 0) { + return word; + } else { + return "_" + word; + } + }); + } + + /** + * 中横杠命名 (kebab-case) + */ + public static String kebabCase(String str) { + return processWords(str, (word, index) -> { + if (index == 0) { + return word; + } else { + return "-" + word; + } + }); + } + + /** + * 函数式接口,用于处理单词连接 + */ + @FunctionalInterface + public interface WordJoiner { + String join(String word, int index); + } +} diff --git a/njzscloud-common/njzscloud-common-email/pom.xml b/njzscloud-common/njzscloud-common-email/pom.xml new file mode 100644 index 0000000..d6b96da --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-email + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + + org.springframework.boot + spring-boot-starter + + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java new file mode 100644 index 0000000..d95f03d --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/MailMessage.java @@ -0,0 +1,234 @@ +package com.njzscloud.common.email; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.Mime; +import jakarta.mail.util.ByteArrayDataSource; +import lombok.Getter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 邮件消息 + */ +@Getter +public class MailMessage { + /** + * 收件人邮箱 + */ + private final List tos; + /** + * 邮件主题 + */ + private final String subject; + /** + * 邮件内容 + */ + private final String content; + /** + * 是否为 HTML + */ + private final boolean html; + /** + * 附件列表,0-->附件名称、1-->附件内容 + */ + private final List> attachmentList; + + /** + * 创建邮件消息 + * + * @param tos 收件人邮箱 + * @param subject 邮件主题 + * @param content 邮件内容 + * @param html 是否为 HTML + * @param attachmentList 附件列表,0-->附件名称、1-->附件内容 + */ + private MailMessage(List tos, String subject, String content, boolean html, List> attachmentList) { + this.tos = tos; + this.subject = subject; + this.content = content; + this.html = html; + this.attachmentList = attachmentList; + } + + /** + * 获取构建器 + * + * @return 构建器对象 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * 构建器 + */ + public static class Builder { + /** + * 收件人邮箱 + */ + private List tos; + /** + * 邮件主题 + */ + private String subject; + /** + * 邮件内容 + */ + private String content; + /** + * 是否为 HTML + */ + private boolean html; + /** + * 附件列表,0-->附件名称、1-->附件内容 + */ + private List> attachmentList; + + /** + * 添加收件人邮箱 + * + * @param to 收件人邮箱 + * @return 构建器对象 + */ + public Builder addTo(String to) { + if (this.tos == null) { + this.tos = new ArrayList<>(); + } + this.tos.add(to); + return this; + } + + /** + * 设置收件人邮箱 + * + * @param tos 收件人邮箱 + * @return 构建器对象 + */ + public Builder tos(List tos) { + this.tos = tos; + return this; + } + + /** + * 设置邮件主题 + * + * @param subject 邮件主题 + * @return 构建器对象 + */ + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + /** + * 设置邮件内容 + * + * @param content 邮件内容(常规内容) + * @return 构建器对象 + */ + public Builder content(String content) { + this.content = content; + this.html = false; + return this; + } + + /** + * 设置邮件内容 + * + * @param content 邮件内容(HTML内容) + * @return 构建器对象 + */ + public Builder htmlContent(String content) { + this.content = content; + this.html = true; + return this; + } + + /** + * 设置附件列表 + * + * @param attachmentList 附件列表,0-->附件名称、1-->附件内容 + * @return 构建器对象 + */ + public Builder attachmentList(List> attachmentList) { + this.attachmentList = attachmentList; + return this; + } + + /** + * 添加附件 + * + * @param filename 附件名称 + * @param in 附件 + * @param mime 内容类型 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, InputStream in, String mime) { + Assert.notBlank(filename, "附件名称不能为空"); + Assert.notNull(in, "附件不能为空"); + Assert.notBlank(mime, "内容类型不能为空"); + if (this.attachmentList == null) { + this.attachmentList = new ArrayList<>(); + } + ByteArrayDataSource dataSource; + try (InputStream is = in) { + dataSource = new ByteArrayDataSource(is, mime); + } catch (IOException e) { + throw Exceptions.error(e, "附件读取失败"); + } + this.attachmentList.add(Tuple2.of(filename, dataSource)); + return this; + } + + /** + * 添加附件 + * + * @param filename 附件名称 + * @param bytes 附件 + * @param mime 内容类型 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, byte[] bytes, String mime) { + return addAttachment(filename, new ByteArrayInputStream(bytes), mime); + } + + /** + * 添加附件,内容类型默认:application/octet-stream + * + * @param filename 附件名称 + * @param bytes 附件 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, byte[] bytes) { + return addAttachment(filename, new ByteArrayInputStream(bytes), Mime.BINARY); + } + + /** + * 添加附件,内容类型默认:application/octet-stream + * + * @param filename 附件名称 + * @param in 附件 + * @return 构建器对象 + */ + public Builder addAttachment(String filename, InputStream in) { + return addAttachment(filename, in, Mime.BINARY); + } + + /** + * 构建邮件消息 + * + * @return MailMessage + */ + public MailMessage build() { + Assert.isTrue(this.tos != null && !this.tos.isEmpty(), "收件人邮箱不能为空"); + Assert.notBlank(this.subject, "邮件主题不能为空"); + return new MailMessage(this.tos, this.subject, this.content, this.html, this.attachmentList); + } + } +} diff --git a/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java new file mode 100644 index 0000000..55823d5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-email/src/main/java/com/njzscloud/common/email/util/EMailUtil.java @@ -0,0 +1,98 @@ +package com.njzscloud.common.email.util; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.email.MailMessage; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.util.ByteArrayDataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.MailAuthenticationException; +import org.springframework.mail.MailException; +import org.springframework.mail.MailParseException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +import java.util.Collections; +import java.util.List; + +/** + * 电子邮件工具 + */ +@Slf4j +public class EMailUtil { + private static final String FROM; + private static final JavaMailSender MAIL_SENDER; + + static { + MAIL_SENDER = SpringUtil.getBean(JavaMailSender.class); + FROM = SpringUtil.getProperty("spring.mail.username"); + } + + /** + * 发送简单文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content 邮件内容 + */ + public void sendSimpleMail(String to, String subject, String content) { + sendSimpleMail(Collections.singletonList(to), subject, content); + } + + /** + * 发送简单文本邮件 + * + * @param tos 接收者邮件 + * @param subject 邮件主题 + * @param content 邮件内容 + */ + public void sendSimpleMail(List tos, String subject, String content) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(FROM); + message.setTo(tos.toArray(new String[0])); + message.setSubject(subject); + message.setText(content); + try { + MAIL_SENDER.send(message); + } catch (MailException e) { + throw Exceptions.error(e, + e instanceof MailParseException ? "邮件消息解析失败" : + e instanceof MailAuthenticationException ? "邮件服务器认证失败" : "邮件发送失败", e); + } + } + + /** + * 发送复杂邮件 + * + * @param mailMessage 消息 + */ + public void sendComplexMail(MailMessage mailMessage) { + MimeMessage message = MAIL_SENDER.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(FROM); + helper.setTo(mailMessage.getTos().toArray(new String[0])); + helper.setSubject(mailMessage.getSubject()); + helper.setText(mailMessage.getContent(), mailMessage.isHtml()); + List> attachmentList = mailMessage.getAttachmentList(); + if (attachmentList != null && !attachmentList.isEmpty()) { + for (Tuple2 dataSource : attachmentList) { + helper.addAttachment(dataSource.get_0(), dataSource.get_1()); + } + } + } catch (MessagingException e) { + throw Exceptions.error(e, "邮件创建失败"); + } + + try { + MAIL_SENDER.send(message); + } catch (MailException e) { + throw Exceptions.error(e, + e instanceof MailParseException ? "邮件消息解析失败" : + e instanceof MailAuthenticationException ? "邮件服务器认证失败" : "邮件发送失败"); + } + } +} diff --git a/njzscloud-common/njzscloud-common-gen/pom.xml b/njzscloud-common/njzscloud-common-gen/pom.xml new file mode 100644 index 0000000..a42fb3c --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-gen + jar + + njzscloud-common-gen + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + com.njzscloud + njzscloud-common-mvc + provided + + + + com.njzscloud + njzscloud-common-mp + provided + + + + com.ibeetl + beetl + 3.19.2.RELEASE + + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplController.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplController.java new file mode 100644 index 0000000..76861ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplController.java @@ -0,0 +1,121 @@ +package com.njzscloud.common.gen; + +import cn.hutool.core.date.DateTime; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.mvc.util.ServletUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Map; + +/** + * 代码模板 + */ +@Slf4j +@RestController +@RequestMapping("/tpl") +@RequiredArgsConstructor +public class TplController { + + private final TplService tplService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody TplEntity tplEntity) { + tplService.add(tplEntity); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody TplEntity tplEntity) { + tplService.modify(tplEntity); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + tplService.del(ids); + return R.success(); + } + + @PostMapping("/download") + public void download(@RequestParam("lang") String lang, + @RequestParam("tableName") String tableName, + @RequestBody(required = false) Map data, + HttpServletResponse response) { + + ByteArrayOutputStream out = tplService.download(lang, tableName, data); + ServletUtil.download(response, out.toByteArray(), Mime.ZIP, DateTime.now() + ".zip"); + } + + @PostMapping("/preview") + public R preview(@RequestParam("lang") String lang, + @RequestParam("tableName") String tableName, + @RequestBody(required = false) Map data) { + + Map> map = tplService.preview(lang, tableName, data); + + return R.success(map); + } + + + /** + * 获取表信息 + */ + @GetMapping("/table/paging") + public R>> tablePaging(PageParam pageParam, + @RequestParam(value = "tableSchema", required = false) String tableSchema, + @RequestParam(value = "tableName", required = false) String tableName + ) { + return R.success(tplService.tablePaging(pageParam, tableSchema, tableName)); + } + + + /** + * 获取所有模板 + */ + @GetMapping("/list_all") + public R listAll(@RequestParam("lang") String lang) { + return R.success(tplService + .list(Wrappers.lambdaQuery(TplEntity.class) + .eq(TplEntity::getLang, lang) + .orderByAsc(TplEntity::getTplName))); + } + + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam("id") Long id) { + return R.success(tplService.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, TplEntity tplEntity) { + return R.success(tplService.paging(pageParam, tplEntity)); + } + +} + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplEntity.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplEntity.java new file mode 100644 index 0000000..7848c16 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplEntity.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.gen; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.gen.support.Tpl; +import com.njzscloud.common.mp.support.handler.j.JsonTypeHandler; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * 代码模板 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_tpl", autoResultMap = true) +public class TplEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 模板名称 + */ + private String tplName; + + private String lang; + + /** + * 模板内容 + */ + @TableField(typeHandler = JsonTypeHandler.class) + private Tpl tpl; + + /** + * 模型数据 + */ + @TableField(typeHandler = JsonTypeHandler.class) + private Map modelData; + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplMapper.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplMapper.java new file mode 100644 index 0000000..6117a4b --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplMapper.java @@ -0,0 +1,23 @@ +package com.njzscloud.common.gen; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Map; + +/** + * 代码模板 + */ +@Mapper +public interface TplMapper extends BaseMapper { + + @Select("SELECT TABLE_SCHEMA tableSchema, TABLE_NAME tableName, TABLE_COMMENT tableComment\n" + + "FROM information_schema.TABLES\n" + + "${ew.customSqlSegment}") + IPage> tablePaging(Page page, @Param("ew") QueryWrapper ew); +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplService.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplService.java new file mode 100644 index 0000000..0c0de92 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/TplService.java @@ -0,0 +1,186 @@ +package com.njzscloud.common.gen; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.gen.support.DbMetaData; +import com.njzscloud.common.gen.support.Generator; +import com.njzscloud.common.gen.support.TemplateEngine; +import com.njzscloud.common.gen.support.Tpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * 代码模板 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TplService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param tplEntity 数据 + */ + @Transactional(rollbackFor = Exception.class) + public void add(TplEntity tplEntity) { + String tplName = tplEntity.getTplName(); + Assert.notBlank(tplName, () -> Exceptions.clierr("模板名称不能为空")); + boolean exists = this.exists(Wrappers.lambdaQuery(TplEntity.class).eq(TplEntity::getTplName, tplName)); + Assert.isFalse(exists, () -> Exceptions.clierr("模板:{} 已存在", tplName)); + Tpl tpl = tplEntity.getTpl(); + if (tpl == null) { + tplEntity.setTpl(new Tpl()); + } + + Map modelData = tplEntity.getModelData(); + if (modelData == null) { + tplEntity.setModelData(MapUtil.empty()); + } + this.save(tplEntity); + } + + /** + * 修改 + * + * @param tplEntity 数据 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(TplEntity tplEntity) { + String tplName = tplEntity.getTplName(); + Assert.notBlank(tplName, () -> Exceptions.clierr("模板名称不能为空")); + Long id = tplEntity.getId(); + boolean exists = this.exists(Wrappers.lambdaQuery(TplEntity.class) + .eq(TplEntity::getTplName, tplName) + .ne(TplEntity::getId, id) + ); + Assert.isFalse(exists, () -> Exceptions.clierr("模板:{} 已存在", tplName)); + TplEntity oldData = this.getById(id); + Assert.notNull(oldData, () -> Exceptions.clierr("未找到要修改数据")); + TemplateEngine.rmEngine(oldData.getTplName()); + this.updateById(tplEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysTplEntity 结果 + */ + public TplEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param tplEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysTplEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, TplEntity tplEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(tplEntity))); + } + + private final DbMetaData dbMetaData; + + public PageResult> tablePaging(PageParam pageParam, String tableSchema, String tableName) { + pageParam.setOrders("TABLE_NAME"); + if (StrUtil.isBlank(tableSchema)) { + tableSchema = dbMetaData.getDatabaseName(); + } + Assert.notBlank(tableSchema, () -> Exceptions.exception("数据库名称获取失败")); + return PageResult.of(baseMapper.tablePaging(pageParam.toPage(), Wrappers.query() + .eq("TABLE_SCHEMA", tableSchema) + .like(StrUtil.isNotBlank(tableName), "TABLE_NAME", tableName) + )); + } + + public Map> preview(String lang, String tableName, Map data) { + Map> map = MapUtil.newHashMap(); + List tplNames = this.list(Wrappers.lambdaQuery(TplEntity.class) + .eq(TplEntity::getLang, lang)) + .stream() + .map(TplEntity::getTplName) + .toList(); + + for (String tplName : tplNames) { + + Tuple2 res = generate(tplName, tableName, data); + + map.put(tplName, MapUtil.builder() + .put("path", res.get_0()) + .put("content", res.get_1()) + .build()); + } + return map; + } + + public ByteArrayOutputStream download(String lang, String tableName, Map data) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + List tplNames = this.list(Wrappers.lambdaQuery(TplEntity.class) + .eq(TplEntity::getLang, lang)) + .stream() + .map(TplEntity::getTplName) + .toList(); + if (tplNames.isEmpty()) { + return out; + } + int size = tplNames.size(); + String[] paths = new String[size]; + InputStream[] ins = new InputStream[size]; + for (int i = 0; i < size; i++) { + String tplName = tplNames.get(i); + Tuple2 res = generate(tplName, tableName, data); + paths[i] = res.get_0(); + ins[i] = new ByteArrayInputStream(res.get_1().getBytes()); + } + ZipUtil.zip(out, paths, ins); + + return out; + } + + private Tuple2 generate(String tplName, String tableName, Map data) { + Tuple3 res = Generator.generate(tplName, tableName, data); + String res0 = res.get_0(); + String res1 = res.get_1(); + String res2 = res.get_2(); + StringBuilder pathBuilder = new StringBuilder(); + if (StrUtil.isNotBlank(res0)) { + pathBuilder.append(res0.trim()); + if (!res0.endsWith("/")) pathBuilder.append("/"); + } + if (StrUtil.isNotBlank(res1)) pathBuilder.append(res1.trim()); + else pathBuilder.append(IdUtil.fastSimpleUUID()).append(".txt"); + return Tuple2.of(pathBuilder.toString(), res2); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java new file mode 100644 index 0000000..652ce4e --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/config/GenAutoConfiguration.java @@ -0,0 +1,31 @@ +package com.njzscloud.common.gen.config; + +import com.njzscloud.common.gen.TplController; +import com.njzscloud.common.gen.TplService; +import com.njzscloud.common.gen.support.DbMetaData; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + + +@Configuration +@MapperScan("com.njzscloud.common.gen") +public class GenAutoConfiguration { + + @Bean + public TplService sysTplService(DbMetaData dbMetaData) { + return new TplService(dbMetaData); + } + + @Bean + public TplController sysTplController(TplService tplService) { + return new TplController(tplService); + } + + @Bean + public DbMetaData dbMetaData(DataSource dataSource) { + return new DbMetaData(dataSource); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java new file mode 100644 index 0000000..f9927e6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/contant/TplCategory.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.gen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:tpl_category + * 字典名称:模板类型 + */ +@Getter +@RequiredArgsConstructor +public enum TplCategory implements DictStr { + Controller("Controller", "Controller"), + Service("Service", "Service"), + Mapper("Mapper", "Mapper"), + MapperXml("MapperXml", "MapperXml"), + Entity("Entity", "Entity"), + ; + private final String val; + private final String txt; +} + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java new file mode 100644 index 0000000..888ceb5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Btl.java @@ -0,0 +1,67 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import org.beetl.core.Context; +import org.beetl.core.Function; + +import java.util.Map; + +public final class Btl { + public static final Function toCamelCase = (Object[] paras, Context ctx) -> StrUtil.toCamelCase(paras[0].toString()); + public static final Function toDashCase = (Object[] paras, Context ctx) -> StrUtil.toSymbolCase(paras[0].toString(), '-'); + public static final Function upperFirst = (Object[] paras, Context ctx) -> StrUtil.upperFirst(paras[0].toString()); + public static final Function isBlank = (Object[] paras, Context ctx) -> StrUtil.isBlank(paras[0].toString()); + public static final Function subAfter = (Object[] paras, Context ctx) -> StrUtil.subAfter(paras[0].toString(), paras[1].toString(), false); + public static final Function javaType = new Function() { + private final Map> map = MapUtil.>builder() + .put("DEFAULT", MapUtil.builder().put("importStatement", "").put("dataType", "").build()) + .put("VARCHAR", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("CHAR", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("LONGTEXT", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("TEXT", MapUtil.builder().put("importStatement", "").put("dataType", "String").build()) + .put("BIT", MapUtil.builder().put("importStatement", "").put("dataType", "Boolean").build()) + .put("TINYINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("SMALLINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("MEDIUMINT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("INT", MapUtil.builder().put("importStatement", "").put("dataType", "Integer").build()) + .put("BIGINT", MapUtil.builder().put("importStatement", "").put("dataType", "Long").build()) + .put("DOUBLE", MapUtil.builder().put("importStatement", "").put("dataType", "Double").build()) + .put("DECIMAL", MapUtil.builder().put("importStatement", "import java.math.BigDecimal;").put("dataType", "BigDecimal").build()) + .put("DATE", MapUtil.builder().put("importStatement", "import java.time.LocalDate;").put("dataType", "LocalDate").build()) + .put("TIME", MapUtil.builder().put("importStatement", "import java.time.LocalTime;").put("dataType", "LocalTime").build()) + .put("DATETIME", MapUtil.builder().put("importStatement", "import java.time.LocalDateTime;").put("dataType", "LocalDateTime").build()) + .build(); + + @Override + public Object call(Object[] paras, Context ctx) { + return map.getOrDefault(paras[0].toString().toUpperCase(), map.get("DEFAULT")); + } + }; + + public static final Function tsType = new Function() { + private final Map> map = MapUtil.>builder() + .put("DEFAULT", MapUtil.builder().put("dataType", "").build()) + .put("VARCHAR", MapUtil.builder().put("dataType", "string").build()) + .put("CHAR", MapUtil.builder().put("dataType", "string").build()) + .put("LONGTEXT", MapUtil.builder().put("dataType", "string").build()) + .put("TEXT", MapUtil.builder().put("dataType", "string").build()) + .put("BIT", MapUtil.builder().put("dataType", "boolean").build()) + .put("TINYINT", MapUtil.builder().put("dataType", "number").build()) + .put("SMALLINT", MapUtil.builder().put("dataType", "number").build()) + .put("MEDIUMINT", MapUtil.builder().put("dataType", "number").build()) + .put("INT", MapUtil.builder().put("dataType", "number").build()) + .put("BIGINT", MapUtil.builder().put("dataType", "string").build()) + .put("DOUBLE", MapUtil.builder().put("dataType", "number").build()) + .put("DECIMAL", MapUtil.builder().put("dataType", "string").build()) + .put("DATE", MapUtil.builder().put("dataType", "string").build()) + .put("TIME", MapUtil.builder().put("dataType", "string").build()) + .put("DATETIME", MapUtil.builder().put("dataType", "string").build()) + .build(); + + @Override + public Object call(Object[] paras, Context ctx) { + return map.getOrDefault(paras[0].toString().toUpperCase(), map.get("DEFAULT")); + } + }; +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java new file mode 100644 index 0000000..5ea4b06 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/DbMetaData.java @@ -0,0 +1,83 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class DbMetaData { + private final DataSource dataSource; + + public String getDatabaseName() { + try (Connection conn = dataSource.getConnection()) { + return conn.getCatalog(); + } catch (Exception e) { + log.error("获取数据库名称失败", e); + return null; + } + } + + public List> getTableMetaData(String tableName) { + try (Connection connection = dataSource.getConnection()) { + return getTables(connection, tableName); + } catch (Exception e) { + log.error("获取数据库元数据失败", e); + return Collections.emptyList(); + } + } + + private List> getTableColumns(Connection conn, String tableName) throws Exception { + List> columns = new ArrayList<>(); + + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet columnsData = metaData.getColumns(null, null, tableName, "%")) { + while (columnsData.next()) { + columns.add(MapUtil.builder() + .put("name", columnsData.getString("COLUMN_NAME")) + .put("dataType", columnsData.getString("TYPE_NAME")) + .put("size", columnsData.getInt("COLUMN_SIZE")) + .put("nullable", columnsData.getInt("NULLABLE") == DatabaseMetaData.columnNullable) + .put("defaultValue", columnsData.getString("COLUMN_DEF")) + .put("comment", columnsData.getString("REMARKS")) + .build() + ); + } + } + try (ResultSet primaryKeyData = metaData.getPrimaryKeys(null, null, tableName)) { + while (primaryKeyData.next()) { + String primaryKeyColumn = primaryKeyData.getString("COLUMN_NAME"); + columns.forEach(column -> column.put("primaryKey", column.get("name").equals(primaryKeyColumn))); + } + } + return columns; + } + + private List> getTables(Connection conn, String tableName) throws Exception { + List> tableInfos = new ArrayList<>(); + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet tablesData = metaData.getTables(null, null, StrUtil.isNotBlank(tableName) ? tableName : "%", new String[]{"TABLE"})) { + while (tablesData.next()) { + String tableName_ = tablesData.getString("TABLE_NAME"); + List> columns = getTableColumns(conn, tableName_); + tableInfos.add(MapUtil.builder() + .put("name", tableName_) + .put("comment", tablesData.getString("REMARKS")) + .put("columns", columns) + .build()); + } + } + return tableInfos; + } + +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java new file mode 100644 index 0000000..0b686be --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Generator.java @@ -0,0 +1,56 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.gen.TplEntity; +import com.njzscloud.common.gen.TplService; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class Generator { + private static final DbMetaData dbMetaData = SpringUtil.getBean(DbMetaData.class); + private static final TplService tplService = SpringUtil.getBean(TplService.class); + + private static Tuple3 generate0(String tplName, String tableName, Map data) { + + Assert.notBlank(tableName, () -> Exceptions.clierr("未指定表名称")); + data.put("tableName", tableName); + List> table = dbMetaData.getTableMetaData(tableName); + TplEntity tplEntity = tplService.getOne(Wrappers.lambdaQuery() + .eq(TplEntity::getTplName, tplName)); + + Tpl tpl = tplEntity.getTpl(); + Map modelData = tplEntity.getModelData(); + HashMap map = new HashMap<>(modelData); + map.put("table", CollUtil.isNotEmpty(table) ? table.getFirst() : MapUtil.empty()); + map.putAll(data); + log.info("数据:{}", Jackson.toJsonStr(map)); + TemplateEngine templateEngine = TemplateEngine.getEngine(tplName, tpl); + String content = templateEngine.renderContent(map); + String dir = templateEngine.renderDir(map); + String filename = templateEngine.renderFilename(map); + return Tuple3.of(dir, StrUtil.isBlank(filename) ? tplName + ".txt" : filename, content); + } + + /** + * + * @param tplName 模板名称 + * @param tableName 表名称 + * @param data 数据 + * @return 0-->文件夹、1-->文件名、2-->内容 + */ + public static Tuple3 generate(String tplName, String tableName, Map data) { + return generate0(tplName, tableName, data == null ? new HashMap<>() : data); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java new file mode 100644 index 0000000..4f02387 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/TemplateEngine.java @@ -0,0 +1,91 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import lombok.extern.slf4j.Slf4j; +import org.beetl.core.Configuration; +import org.beetl.core.GroupTemplate; +import org.beetl.core.Template; +import org.beetl.core.exception.BeetlException; +import org.beetl.core.exception.ErrorInfo; +import org.beetl.core.resource.MapResourceLoader; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class TemplateEngine { + private static final GroupTemplate GT; + private static final MapResourceLoader LOADER = new MapResourceLoader(); + private static final Map ENGINE = new ConcurrentHashMap<>(); + + static { + GroupTemplate getTemplate_; + try { + Configuration cfg = Configuration.defaultConfiguration(); + getTemplate_ = new GroupTemplate(LOADER, cfg); + getTemplate_.registerFunction("toCamelCase", Btl.toCamelCase); + getTemplate_.registerFunction("toDashCase", Btl.toDashCase); + getTemplate_.registerFunction("upperFirst", Btl.upperFirst); + getTemplate_.registerFunction("isBlank", Btl.isBlank); + getTemplate_.registerFunction("subAfter", Btl.subAfter); + getTemplate_.registerFunction("javaType", Btl.javaType); + getTemplate_.registerFunction("tsType", Btl.tsType); + getTemplate_.setErrorHandler(TemplateEngine::processException); + } catch (IOException e) { + getTemplate_ = null; + log.error("模板引擎初始化失败", e); + } + GT = getTemplate_; + } + + private final String tplName; + + private TemplateEngine(String tplName, Tpl tpl) { + this.tplName = tplName; + LOADER.put(tplName + "-content", tpl.getContent()); + LOADER.put(tplName + "-dir", tpl.getDir()); + LOADER.put(tplName + "-filename", tpl.getFilename()); + } + + private static void processException(BeetlException beeException, GroupTemplate groupTemplate, Writer writer) { + ErrorInfo errorInfo = beeException.toErrorInfo(); + throw Exceptions.error(MapUtil.builder() + .put("type", errorInfo.type) + .put("line", errorInfo.errorTokenLine) + .put("token", errorInfo.errorTokenText) + .put("msg", errorInfo.msg) + .build()); + } + + public static TemplateEngine getEngine(String tplName, Tpl tpl) { + return ENGINE.computeIfAbsent(tplName, key -> new TemplateEngine(tplName, tpl)); + } + + public static void rmEngine(String tplName) { + ENGINE.remove(tplName); + LOADER.remove(tplName + "-content"); + LOADER.remove(tplName + "-dir"); + LOADER.remove(tplName + "-filename"); + } + + public String render(String tplName, Map data) { + Template template = GT.getTemplate(tplName); + template.binding(data); + return template.render(); + } + + public String renderContent(Map data) { + return render(tplName + "-content", data); + } + + public String renderDir(Map data) { + return render(tplName + "-dir", data); + } + + public String renderFilename(Map data) { + return render(tplName + "-filename", data); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java new file mode 100644 index 0000000..b5c6510 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/java/com/njzscloud/common/gen/support/Tpl.java @@ -0,0 +1,27 @@ +package com.njzscloud.common.gen.support; + +import cn.hutool.core.util.StrUtil; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Setter +@ToString +@Accessors(chain = true) +public class Tpl { + private String content; + private String dir; + private String filename; + + public String getContent() { + return content == null ? "" : content; + } + + public String getDir() { + return StrUtil.isBlank(dir) ? "" : dir; + } + + public String getFilename() { + return StrUtil.isBlank(filename) ? "" : filename; + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..63ca111 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.gen.config.GenAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/controller.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/controller.btl new file mode 100644 index 0000000..423e238 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/controller.btl @@ -0,0 +1,75 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +%> +package ${basePackage}.${moduleName}.controller; + +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import java.util.List; +import ${basePackage}.${moduleName}.pojo.entity.${entityClass}; +import ${basePackage}.${moduleName}.service.${serviceClass}; + +/** + * ${table.comment} + */ +@Slf4j +@RestController +@RequestMapping("/${baseUrl}") +@RequiredArgsConstructor +public class ${controllerClass} { + private final ${serviceClass} ${serviceInstance}; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody ${entityClass} ${entityInstance}) { + ${serviceInstance}.add(${entityInstance}); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody ${entityClass} ${entityInstance}) { + ${serviceInstance}.modify(${entityInstance}); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + ${serviceInstance}.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R<${entityClass}> detail(@RequestParam Long id) { + return R.success(${serviceInstance}.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, ${entityClass} ${entityInstance}) { + return R.success(${serviceInstance}.paging(pageParam, ${entityInstance})); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/entity.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/entity.btl new file mode 100644 index 0000000..5ee6d59 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/entity.btl @@ -0,0 +1,56 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +%> +package ${basePackage}.${moduleName}.pojo.entity; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import com.baomidou.mybatisplus.annotation.*; +import lombok.ToString; +<%for(column in table.columns) { + var map = javaType(column.dataType); +%> +<%if(!isBlank(map.importStatement)){%> +${map.importStatement} +<%}%> +<%}%> + +/** + * ${table.comment} + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("${table.name}") +public class ${entityClass} { + +<% +for(column in table.columns) { + var map = javaType(column.dataType); +%> + /** + * ${column.comment} + */ + <%if(column.primaryKey){%> + @TableId(type = IdType.ASSIGN_ID) + <%}%> + <%if(column.name == "creator_id" || column.name == "create_time"){%> + @TableId(fill = FieldFill.INSERT) + <%}else if(column.name == "modifier_id" || column.name == "modify_time"){%> + @TableField(fill = FieldFill.INSERT_UPDATE) + <%}else if(column.name == "deleted"){%> + @TableLogic + <%}%> + private ${map.dataType} ${toCamelCase(column.name)}; + +<%}%> +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper.btl new file mode 100644 index 0000000..f0d8650 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper.btl @@ -0,0 +1,22 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +%> +package ${basePackage}.${moduleName}.mapper; + +import org.apache.ibatis.annotations.Mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import ${basePackage}.${moduleName}.pojo.entity.${entityClass}; + +/** + * ${table.comment} + */ +@Mapper +public interface ${mapperClass} extends BaseMapper<${entityClass}> { +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper_xml.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper_xml.btl new file mode 100644 index 0000000..12b9bf4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/mapper_xml.btl @@ -0,0 +1,14 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +%> + + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/service.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/service.btl new file mode 100644 index 0000000..4eb9437 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/java/service.btl @@ -0,0 +1,69 @@ +<% +var entityName = toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)); +var controllerClass = upperFirst(entityName) + "Controller"; +var entityClass = upperFirst(entityName) + "Entity"; +var entityInstance = entityName + "Entity"; +var serviceClass = upperFirst(entityName) + "Service"; +var serviceInstance = entityName + "Service"; +var mapperClass = upperFirst(entityName) + "Mapper"; +var baseUrl = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +%> +package ${basePackage}.${moduleName}.service; + +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import java.util.List; +import ${basePackage}.${moduleName}.pojo.entity.${entityClass}; +import ${basePackage}.${moduleName}.mapper.${mapperClass}; + +/** + * ${table.comment} + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ${serviceClass} extends ServiceImpl<${mapperClass}, ${entityClass}> implements IService<${entityClass}> { + + /** + * 新增 + */ + public void add(${entityClass} ${entityInstance}) { + this.save(${entityInstance}); + } + + /** + * 修改 + */ + public void modify(${entityClass} ${entityInstance}) { + this.updateById(${entityInstance}); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public ${entityClass} detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult<${entityClass}> paging(PageParam pageParam, ${entityClass} ${entityInstance}) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.<${entityClass}>query(${entityInstance}))); + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json new file mode 100644 index 0000000..c3082df --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/tpl.json @@ -0,0 +1,116 @@ +{ + "ts": [ + { + "modelData": { + "prefix": "", + "basePackage": "", + "moduleName": "", + "subModuleName": "" + }, + "tplName": "page.vue", + "tpl": { + "dir": "${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>", + "filename": "${pageVueName}", + "content": "" + } + }, + { + "modelData": { + "moduleName": "", + "subModuleName": "" + }, + "tplName": "form.vue", + "tpl": { + "dir": "${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>", + "filename": "${formVueName}", + "content": "" + } + }, + { + "modelData": { + "moduleName": "", + "subModuleName": "" + }, + "tplName": "page.ts", + "tpl": { + "dir": "${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>", + "filename": "${pageTsName}", + "content": "" + } + }, + { + "modelData": { + "moduleName": "", + "subModuleName": "" + }, + "tplName": "api.ts", + "tpl": { + "dir": "${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>", + "filename": "${apiTsName}", + "content": "" + } + }, + { + "modelData": { + "moduleName": "", + "subModuleName": "" + }, + "tplName": "d.ts", + "tpl": { + "dir": "${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>", + "filename": "${dTsName}", + "content": "" + } + } + ], + "java": [ + { + "modelData": { + "basePackage": "", + "moduleName": "" + }, + "tplName": "entity.java", + "tpl": { + "dir": "pojo/entity", + "filename": "${upperFirst(toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)))}Entity.java", + "content": "" + } + }, + { + "modelData": {}, + "tplName": "mapper.java", + "tpl": { + "dir": "mapper", + "filename": "${upperFirst(toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)))}Mapper.java", + "content": "" + } + }, + { + "modelData": {}, + "tplName": "mapper.xml", + "tpl": { + "dir": "", + "filename": "${upperFirst(toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)))}Mapper.xml", + "content": "" + } + }, + { + "modelData": {}, + "tplName": "service.java", + "tpl": { + "dir": "service", + "filename": "${upperFirst(toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)))}Service.java", + "content": "" + } + }, + { + "modelData": {}, + "tplName": "controller.java", + "tpl": { + "dir": "controller", + "filename": "${upperFirst(toCamelCase(isBlank(prefix) ? table.name : subAfter(table.name, prefix)))}Controller.java", + "content": "" + } + } + ] +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/api.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/api.btl new file mode 100644 index 0000000..d038c49 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/api.btl @@ -0,0 +1,34 @@ +<% +var ulc = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +var lcc = toCamelCase(ulc); +var ucc = upperFirst(lcc); +var dc = toDashCase(lcc); + +var pageVueName = ucc + ".vue"; +var formVueName = ucc + "Form.vue"; +var pageTsName = dc + ".ts"; +var apiTsName = dc + "-api.ts"; +var dTsName = dc + ".d.ts"; +%> +import { + get, + post +} from '@/common/utils/http-util.ts' + +export default { + paging(data: ${ucc}Types.Search${ucc}Param) { + return get>('/${ulc}/paging', data) + }, + detail(id: string) { + return get<${ucc}Types.Search${ucc}Result>('/${ulc}/detail', {id}) + }, + add(data: ${ucc}Types.Add${ucc}Param) { + return post('/${ulc}/add', data) + }, + modify(data: ${ucc}Types.Modify${ucc}Param) { + return post('/${ulc}/modify', data) + }, + del(ids: string[]) { + return post('/${ulc}/del', ids) + }, +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/d.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/d.btl new file mode 100644 index 0000000..5548772 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/d.btl @@ -0,0 +1,57 @@ +<% +var ulc = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +var lcc = toCamelCase(ulc); +var ucc = upperFirst(lcc); +var dc = toDashCase(lcc); + +var pageVueName = ucc + ".vue"; +var formVueName = ucc + "Form.vue"; +var pageTsName = dc + ".ts"; +var apiTsName = dc + "-api.ts"; +var dTsName = dc + ".d.ts"; +%> +export {} + +declare global { + namespace ${ucc}Types { + interface Search${ucc}Param extends G.PageParam { + <% + for(column in table.columns) { + var map = tsType(column.dataType); + %> + // ${column.comment} + ${toCamelCase(column.name)}?: ${map.dataType} + <%}%> + } + + interface Search${ucc}Result { + <% + for(column in table.columns) { + var map = tsType(column.dataType); + %> + // ${column.comment} + ${toCamelCase(column.name)}?: ${map.dataType} + <%}%> + } + + interface Add${ucc}Param { + <% + for(column in table.columns) { + var map = tsType(column.dataType); + %> + // ${column.comment} + ${toCamelCase(column.name)}?: ${map.dataType} + <%}%> + } + + interface Modify${ucc}Param { + <% + for(column in table.columns) { + var map = tsType(column.dataType); + %> + // ${column.comment} + ${toCamelCase(column.name)}?: ${map.dataType} + <%}%> + } + } +} diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/form_vue.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/form_vue.btl new file mode 100644 index 0000000..6d9ab11 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/form_vue.btl @@ -0,0 +1,120 @@ +<% +var ulc = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +var lcc = toCamelCase(ulc); +var ucc = upperFirst(lcc); +var dc = toDashCase(lcc); + +var pageVueName = ucc + ".vue"; +var formVueName = ucc + "Form.vue"; +var pageTsName = dc + ".ts"; +var apiTsName = dc + "-api.ts"; +var dTsName = dc + ".d.ts"; +%> + + + + + diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page.btl new file mode 100644 index 0000000..a00a8d1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page.btl @@ -0,0 +1,15 @@ +<% +var ulc = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +var lcc = toCamelCase(ulc); +var ucc = upperFirst(lcc); +var dc = toDashCase(lcc); + +var pageVueName = ucc + ".vue"; +var formVueName = ucc + "Form.vue"; +var pageTsName = dc + ".ts"; +var apiTsName = dc + "-api.ts"; +var dTsName = dc + ".d.ts"; +%> +export default { + component: () => import('@/pages/${moduleName}<%if(subModuleName != null){%>/${subModuleName}<%}%>/${pageVueName}'), +} as RouterTypes.RouteConfig diff --git a/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page_vue.btl b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page_vue.btl new file mode 100644 index 0000000..fe75122 --- /dev/null +++ b/njzscloud-common/njzscloud-common-gen/src/main/resources/templates/ts/page_vue.btl @@ -0,0 +1,206 @@ +<% +var ulc = isBlank(prefix) ? table.name : subAfter(table.name, prefix); +var lcc = toCamelCase(ulc); +var ucc = upperFirst(lcc); +var dc = toDashCase(lcc); + +var pageVueName = ucc + ".vue"; +var formVueName = ucc + "Form.vue"; +var pageTsName = dc + ".ts"; +var apiTsName = dc + "-api.ts"; +var dTsName = dc + ".d.ts"; +%> + + + + + diff --git a/njzscloud-common/njzscloud-common-http/pom.xml b/njzscloud-common/njzscloud-common-http/pom.xml new file mode 100644 index 0000000..5d72653 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + njzscloud-common-http + jar + + + + com.njzscloud + njzscloud-common-core + provided + + + + + com.squareup.okhttp3 + okhttp + + + diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClient.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClient.java new file mode 100644 index 0000000..8908bb0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClient.java @@ -0,0 +1,209 @@ +package com.njzscloud.common.http; + +import cn.hutool.core.collection.CollUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.http.config.HttpClientProperties; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.support.ResponseInfo; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * HTTP 客户端 + * + * @see okhttp3.OkHttpClient + */ +@Slf4j +public final class HttpClient { + private static volatile HttpClient DEFAULT; + + private final OkHttpClient OK_HTTP_CLIENT; + + public HttpClient(HttpClientProperties httpClientProperties) { + this(httpClientProperties, null); + } + + public HttpClient(OkHttpClient okHttpClient) { + OK_HTTP_CLIENT = okHttpClient; + } + + public HttpClient(HttpClientProperties httpClientProperties, ExecutorService executorService) { + Duration callTimeout = httpClientProperties.getCallTimeout(); + Duration keepAliveTime = httpClientProperties.getKeepAliveTime(); + long keepAliveTimeSeconds = keepAliveTime.getSeconds(); + int maxIdleConnections = httpClientProperties.getMaxIdleConnections(); + Duration readTimeout = httpClientProperties.getReadTimeout(); + Duration connectTimeout = httpClientProperties.getConnectTimeout(); + + ConnectionPool connectionPool = new ConnectionPool(maxIdleConnections, keepAliveTimeSeconds, TimeUnit.SECONDS); + + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .callTimeout(callTimeout) + .readTimeout(readTimeout) + .connectTimeout(connectTimeout) + .connectionPool(connectionPool); + if (executorService != null) { + Dispatcher dispatcher = new Dispatcher(executorService); + builder.dispatcher(dispatcher); + } + + this.OK_HTTP_CLIENT = builder.build(); + } + + private HttpClient() { + this(HttpClientProperties.DEFAULT); + } + + public static synchronized HttpClient defaultHttpClient() { + if (DEFAULT == null) DEFAULT = new HttpClient(); + return DEFAULT; + } + + /* private Request buildRequest(HttpServer httpServer, + HttpEndpoint httpEndpoint, + RequestParamBuilder requestParamBuilder, + RequestInterceptor requestInterceptor) { + + + if (requestInterceptor != null) { + RequestParam processed = requestInterceptor.process(httpServer, httpEndpoint, requestParamBuilder); + if (processed != null) requestParam = processed; + } + + HttpMethod httpMethod = httpEndpoint.httpMethod; + String url = httpEndpoint.urlTpl; + BodyParam bodyParam = requestParam.bodyParam; + + + Map pathParamMap = requestParam.pathParam.getParam(); + if (CollUtil.isNotEmpty(pathParamMap)) { + url = StrUtil.format(url, pathParamMap); + } + + Map queryParamMap = requestParam.queryParam.getParam(); + String query = Param.kvStr(queryParamMap); + if (StrUtil.isNotBlank(query)) { + query = "?" + query; + } + url = url + query; + + Headers headers = null; + Map headerParamMap = requestParam.headerParam.getParam(); + if (CollUtil.isNotEmpty(headerParamMap)) { + Headers.Builder headerBuilder = new Headers.Builder(); + Set> entries = headerParamMap.entrySet(); + for (Map.Entry entry : entries) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value != null) headerBuilder.set(key, value.toString()); + } + headers = headerBuilder.build(); + } + + + RequestBody postBody = null; + if (httpMethod != HttpMethod.GET) { + byte[] param = bodyParam.getParam(); + postBody = RequestBody.create(param, MediaType.parse(bodyParam.contentType)); + } + + Request.Builder requestBuilder = new Request.Builder() + .url(httpServer + url) + .method(httpMethod.name(), postBody); + + if (headers != null) { + requestBuilder.headers(headers); + } + + return requestBuilder.build(); + } + + public T execute( + HttpServer httpServer, + HttpEndpoint endpoint, + RequestParamBuilder requestParamBuilder, + Type responseType, + RequestInterceptor requestInterceptor, + ResponseInterceptor responseInterceptor + ) { + ResponseResult.ResponseResultBuilder responseResultBuilder = ResponseResult.builder(); + + Response response = null; + ResponseBody responseBody = null; + Integer code = null; + String message = null; + Headers headers = null; + byte[] body = new byte[0]; + + try { + Request request = buildRequest(httpServer, endpoint, requestParamBuilder, requestInterceptor); + Call call = this.OK_HTTP_CLIENT.newCall(request); + response = call.execute(); + responseBody = response.body(); + if (responseBody != null) { + body = responseBody.bytes(); + } + code = response.code(); + message = response.message(); + headers = response.headers(); + + responseResultBuilder. + code(code) + .status(message) + .headers(headers.toMultimap()) + .body(body) + .build(); + } catch (Exception e) { + log.error("", e); + responseResultBuilder.e(e); + } finally { + if (responseBody != null) responseBody.close(); + if (response != null) response.close(); + } + + return responseInterceptor.process( + responseType, + httpServer, endpoint, requestParam, + responseResultBuilder.build()); + } */ + + + public ResponseInfo execute(HttpMethod httpMethod, String url, Map headers, Tuple2 body) { + Request.Builder requestBuilder = new Request.Builder().url(url); + + if (httpMethod != HttpMethod.GET) { + requestBuilder.method(httpMethod.name(), + RequestBody.create(body.get_1(), + MediaType.parse(body.get_0()) + )); + } + + if (CollUtil.isNotEmpty(headers)) { + Headers.Builder headerBuilder = new Headers.Builder(); + headers.forEach(headerBuilder::set); + requestBuilder.headers(headerBuilder.build()); + } + + try ( + Response response = this.OK_HTTP_CLIENT + .newCall(requestBuilder.build()) + .execute(); + ResponseBody responseBody = response.body() + ) { + int code = response.code(); + String message = response.message(); + Map> responseHeaders = response.headers().toMultimap(); + byte[] bytes = responseBody == null ? new byte[0] : responseBody.bytes(); + return ResponseInfo.create(code, message, responseHeaders, bytes); + } catch (Exception e) { + log.error("", e); + return ResponseInfo.create(0, "http 请求失败", null, null); + } + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClientDecorator.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClientDecorator.java new file mode 100644 index 0000000..ef8ac21 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/HttpClientDecorator.java @@ -0,0 +1,220 @@ +package com.njzscloud.common.http; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.SimpleCache; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.http.annotation.GetEndpoint; +import com.njzscloud.common.http.annotation.PostEndpoint; +import com.njzscloud.common.http.annotation.RemoteServer; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.interceptor.RequestInterceptor; +import com.njzscloud.common.http.interceptor.ResponseInterceptor; +import com.njzscloud.common.http.resolver.*; +import com.njzscloud.common.http.support.RequestInfo; +import com.njzscloud.common.http.support.ResponseInfo; +import com.njzscloud.common.http.support.ResponseResult; +import lombok.extern.slf4j.Slf4j; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 服务装饰器, 用于 "接口注解式" HTTP 请求配置, 使用 JDK 动态代理进行接口装饰 + *

注: 在使用异步请求时, 接口方法的返回值不能是 void, 除非 HTTP 请求确实没有返回值

+ */ +@SuppressWarnings("unchecked") +public class HttpClientDecorator { + private static final SimpleCache, Object> ENHANCER_CACHE = new SimpleCache<>(); + + private final HttpClient HTTP_CLIENT; + + public HttpClientDecorator(HttpClient httpClient) { + HTTP_CLIENT = httpClient; + } + + public T decorate(Class clazz) { + return (T) ENHANCER_CACHE.get(clazz, () -> Proxy.newProxyInstance( + clazz.getClassLoader(), + new Class[]{clazz}, + new MethodInterceptorImpl(clazz, HTTP_CLIENT))); + } + + @Slf4j + private static class MethodInterceptorImpl implements InvocationHandler { + private static final Pattern ADDR_PATTERN = Pattern.compile("(?http(?:s)?):\\/\\/(?[0-9a-zA-Z_\\-\\.]+)(?::(?[0-9]+))?(?\\/\\S*)*"); + private final HttpClient HTTP_CLIENT; + private final String baseUrl; + private final String name; + private final RequestInterceptor requestInterceptor; + private final ResponseInterceptor responseInterceptor; + + public MethodInterceptorImpl(Class clazz, HttpClient httpClient) { + RemoteServer anno = clazz.getAnnotation(RemoteServer.class); + String name = anno.name(); + if (StrUtil.isBlank(name)) { + this.name = clazz.getName(); + } else { + this.name = name; + + } + String value = anno.value(); + if (value.startsWith("${") && value.endsWith("}")) { + value = SpringUtil.getProperty(value.substring(2, value.length() - 1)); + } + if (StrUtil.isBlank(value)) { + throw Exceptions.error("地址不合法"); + } + Matcher matcher = ADDR_PATTERN.matcher(value); + Assert.isTrue(matcher.matches(), () -> Exceptions.error("地址不合法")); + baseUrl = value; + Class requestedInterceptorClazz = anno.requestInterceptor(); + Class responseInterceptorClazz = anno.responseInterceptor(); + requestInterceptor = ReflectUtil.newInstance(requestedInterceptorClazz); + responseInterceptor = ReflectUtil.newInstance(responseInterceptorClazz); + + HTTP_CLIENT = httpClient; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + HttpMethod httpMethod; + String urlTpl; + String desc; + String methodName = method.getName(); + switch (methodName) { + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "equals" -> { + return proxy == args[0]; + } + case "toString" -> { + return name; + } + } + + GetEndpoint getEndpoint = method.getAnnotation(GetEndpoint.class); + if (getEndpoint == null) { + PostEndpoint postEndpoint = method.getAnnotation(PostEndpoint.class); + if (postEndpoint == null) { + return method.invoke(this, args); + } else { + httpMethod = HttpMethod.POST; + urlTpl = baseUrl + postEndpoint.value(); + desc = postEndpoint.desc(); + } + } else { + httpMethod = HttpMethod.GET; + urlTpl = baseUrl + getEndpoint.value(); + desc = getEndpoint.desc(); + } + + Object[] additional = requestInterceptor.process(httpMethod, urlTpl, args); + + Parameter[] parameters = method.getParameters(); + + PathParamResolver pathParamResolver = new PathParamResolver(); + QueryParamResolver queryParamResolver = new QueryParamResolver(); + HeaderParamResolver headerParamResolver = new HeaderParamResolver(); + BodyParamResolver bodyParamResolver = null; + JsonBodyParamResolver jsonBodyParamResolver = null; + XmlBodyParamResolver xmlBodyParamResolver = null; + FormBodyParamResolver formBodyParamResolver = null; + MultiBodyParamResolver multiBodyParamResolver = null; + + List> paramResolvers = ListUtil.list(false, + pathParamResolver, + queryParamResolver, + headerParamResolver + ); + + if (httpMethod != HttpMethod.GET) { + jsonBodyParamResolver = new JsonBodyParamResolver(); + xmlBodyParamResolver = new XmlBodyParamResolver(); + formBodyParamResolver = new FormBodyParamResolver(); + multiBodyParamResolver = new MultiBodyParamResolver(); + bodyParamResolver = new BodyParamResolver(); + paramResolvers.add(jsonBodyParamResolver); + paramResolvers.add(xmlBodyParamResolver); + paramResolvers.add(formBodyParamResolver); + paramResolvers.add(multiBodyParamResolver); + paramResolvers.add(bodyParamResolver); + } + + if (additional != null) { + for (Object o : additional) { + for (ParamResolver resolver : paramResolvers) { + resolver.resolve(o); + } + } + } + + for (int i = 0; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + for (ParamResolver resolver : paramResolvers) { + resolver.resolve(parameter, args[i]); + } + } + + urlTpl = pathParamResolver.resolve(httpMethod, urlTpl); + urlTpl = queryParamResolver.resolve(httpMethod, urlTpl); + Map headers = headerParamResolver.resolve(httpMethod, urlTpl); + Tuple2 body = null; + if (httpMethod != HttpMethod.GET) { + body = jsonBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = xmlBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = formBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = multiBodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = bodyParamResolver.resolve(httpMethod, urlTpl); + if (body == null) body = Tuple2.of("", new byte[0]); + } + + ResponseInfo responseInfo = HTTP_CLIENT.execute(httpMethod, urlTpl, headers, body); + + RequestInfo requestInfo = RequestInfo.create(desc, httpMethod, urlTpl, headers, body); + + Type genericReturnType = method.getGenericReturnType(); + + Type returnType; + Type rawType; + + if (genericReturnType instanceof ParameterizedType typeWrap) { + + rawType = typeWrap.getRawType(); + + if (rawType == ResponseResult.class) { + returnType = typeWrap.getActualTypeArguments()[0]; + } else { + returnType = typeWrap; + } + } else { + returnType = genericReturnType; + rawType = genericReturnType; + } + + Object res = responseInterceptor.process(requestInfo, responseInfo, returnType); + + if (ResponseResult.class == rawType) { + return ResponseResult + .builder() + .code(responseInfo.code()) + .status(responseInfo.message()) + .headers(responseInfo.header()) + .body(res) + .e(responseInfo.e()) + .build(); + } + + return res; + } + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/BodyParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/BodyParam.java new file mode 100644 index 0000000..0c56aff --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/BodyParam.java @@ -0,0 +1,23 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.BodyParamProcessor; +import com.njzscloud.common.http.processor.DefaultBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface BodyParam { + String contentType() default ""; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/FormBodyParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/FormBodyParam.java new file mode 100644 index 0000000..37db154 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/FormBodyParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.FormBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormBodyParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default FormBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/GetEndpoint.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/GetEndpoint.java new file mode 100644 index 0000000..dd020fd --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/GetEndpoint.java @@ -0,0 +1,28 @@ +package com.njzscloud.common.http.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +/** + * GET 请求端点注解 + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GetEndpoint { + /** + * 端点地址 + * + * @return String + */ + String value() default ""; + + /** + * 端点描述 + * + * @return String + */ + String desc() default ""; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/HeaderParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/HeaderParam.java new file mode 100644 index 0000000..5c66a5b --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/HeaderParam.java @@ -0,0 +1,33 @@ +package com.njzscloud.common.http.annotation; + + +import com.njzscloud.common.http.processor.DefaultHeaderParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HeaderParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultHeaderParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/JsonBodyParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/JsonBodyParam.java new file mode 100644 index 0000000..3d90850 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/JsonBodyParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.DefaultJsonBodyParamProcessor; +import com.njzscloud.common.http.processor.JsonBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonBodyParam { + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultJsonBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/MultiBodyParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/MultiBodyParam.java new file mode 100644 index 0000000..51d4bc5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/MultiBodyParam.java @@ -0,0 +1,37 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.MultiBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MultiBodyParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + String filename() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + String contentType() default ""; + + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default MultiBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PathParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PathParam.java new file mode 100644 index 0000000..a70d568 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PathParam.java @@ -0,0 +1,34 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.DefaultPathParamProcessor; +import com.njzscloud.common.http.processor.PathParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PathParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultPathParamProcessor.class; + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PostEndpoint.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PostEndpoint.java new file mode 100644 index 0000000..b8e0590 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/PostEndpoint.java @@ -0,0 +1,28 @@ +package com.njzscloud.common.http.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +/** + * POST 请求端点注解 + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostEndpoint { + /** + * 端点地址 + * + * @return String + */ + String value() default ""; + + /** + * 端点描述 + * + * @return String + */ + String desc() default ""; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/QueryParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/QueryParam.java new file mode 100644 index 0000000..45b1b25 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/QueryParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.DefaultQueryParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface QueryParam { + /** + * 字段名 + * + * @return String + */ + String value() default ""; + + String format() default ""; + + RoundingMode roundingMode() default RoundingMode.HALF_UP; + + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultQueryParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/RemoteServer.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/RemoteServer.java new file mode 100644 index 0000000..c35b254 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/RemoteServer.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.http.annotation; + + +import com.njzscloud.common.http.interceptor.CompositeInterceptor; +import com.njzscloud.common.http.interceptor.RequestInterceptor; +import com.njzscloud.common.http.interceptor.ResponseInterceptor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +/** + * HTTP 服务器配置 + */ +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RemoteServer { + /** + * 服务器地址, 如, http://localhost:80/api + * + * @return String + */ + String value() default ""; + + String name() default ""; + + /** + * 请求拦截器, 在请求之前触发 + * + * @return RequestInterceptor.class + * @see RequestInterceptor + * @see CompositeInterceptor + */ + Class requestInterceptor() default CompositeInterceptor.class; + + /** + * 响应拦截器, 在请求之后触发 + * + * @return ResponseInterceptor.class + * @see ResponseInterceptor + * @see CompositeInterceptor + */ + Class responseInterceptor() default CompositeInterceptor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/XmlBodyParam.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/XmlBodyParam.java new file mode 100644 index 0000000..85b0f23 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/annotation/XmlBodyParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.http.annotation; + +import com.njzscloud.common.http.processor.DefaultXmlBodyParamProcessor; +import com.njzscloud.common.http.processor.XmlBodyParamProcessor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({TYPE, FIELD, PARAMETER, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface XmlBodyParam { + /** + * 参数处理器 + * + * @return ParamProcessor + */ + Class processor() default DefaultXmlBodyParamProcessor.class; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientAutoConfiguration.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientAutoConfiguration.java new file mode 100644 index 0000000..09a407f --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.http.config; + +import com.njzscloud.common.http.HttpClient; +import com.njzscloud.common.http.HttpClientDecorator; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * http客户端自动配置 + */ +@Configuration +@EnableConfigurationProperties(HttpClientProperties.class) +public class HttpClientAutoConfiguration { + + @Bean + public HttpClient httpClient(HttpClientProperties httpClientProperties) { + return new HttpClient(httpClientProperties); + } + + @Bean + public HttpClientDecorator httpClientDecorator(HttpClient httpClient) { + return new HttpClientDecorator(httpClient); + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientProperties.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientProperties.java new file mode 100644 index 0000000..3552a3d --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/config/HttpClientProperties.java @@ -0,0 +1,62 @@ +package com.njzscloud.common.http.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * HTTP 客户端配置 + */ +@Getter +@Setter +@Accessors(chain = true) +@ConfigurationProperties("http-client") +public class HttpClientProperties { + + public static final HttpClientProperties DEFAULT = new HttpClientProperties(); + + /** + * 调用的超时 + * 覆盖解析 DNS、连接、写入请求正文、服务器处理和读取响应正文等。如果调用需要重定向或重试,所有都必须在一个超时期限内完成。 + * 默认 0,0 表示不限制 + */ + private Duration callTimeout = Duration.ofSeconds(0); + + /** + * 读取超时 + * 默认 10s,0 表示不限制 + */ + private Duration readTimeout = Duration.ofSeconds(10); + + /** + * 连接超时 + * TCP 套接字连接到目标主机超时时间 + * 默认 10s,0 表示不限制 + */ + private Duration connectTimeout = Duration.ofSeconds(10); + + /** + * 最大活跃连接数, IP 和 port 相同的 HTTP 请求通常共享同一个连接 + * 默认 500 + */ + private int maxIdleConnections = 500; + + /** + * 连接空闲时间 + * 默认 10min + */ + private Duration keepAliveTime = Duration.ofMinutes(10); + + /** + * 是否禁用 SSL 验证, 默认 false 不禁用 + */ + private boolean disableSslValidation = false; + + /** + * 是否允许重定向, 默认 true 允许 + */ + private boolean followRedirects = true; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/constant/HttpMethod.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/constant/HttpMethod.java new file mode 100644 index 0000000..ca349cf --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/constant/HttpMethod.java @@ -0,0 +1,8 @@ +package com.njzscloud.common.http.constant; + +/** + * HTTP 方法 + */ +public enum HttpMethod { + GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/CompositeInterceptor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/CompositeInterceptor.java new file mode 100644 index 0000000..547f41e --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/CompositeInterceptor.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.http.interceptor; + +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.support.RequestInfo; +import com.njzscloud.common.http.support.ResponseInfo; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Type; + + +/** + * 组合式拦截器, 响应拦截器默认解析 JSON 类型参数 + * + * @see RequestInterceptor + * @see ResponseInterceptor + */ +@Slf4j +public class CompositeInterceptor implements RequestInterceptor, ResponseInterceptor { + + @Override + public Object[] process(HttpMethod method, String url, Object[] args) { + return null; + } + + @Override + public Object process(RequestInfo requestInfo, ResponseInfo responseInfo, Type responseType) { + log.info("响应拦截器: {}、{}、{}", Jackson.toJsonStr(requestInfo), new String(responseInfo.body()), responseType); + Object data = null; + + if (responseInfo.success()) { + if (responseInfo.body() != null) { + if (responseType == String.class) { + data = new String(responseInfo.body()); + } else { + data = Jackson.toBean(responseInfo.body(), responseType); + } + } + } else { + log.error("HTTP请求失败"); + } + + return data; + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/RequestInterceptor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/RequestInterceptor.java new file mode 100644 index 0000000..713e435 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/RequestInterceptor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.http.interceptor; + + +import com.njzscloud.common.http.constant.HttpMethod; + +/** + * 请求拦截器, 在请求之前触发, 可修改请求参数 + */ +public interface RequestInterceptor { + Object[] process(HttpMethod method, String url, Object[] args); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/ResponseInterceptor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/ResponseInterceptor.java new file mode 100644 index 0000000..6d0593e --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/interceptor/ResponseInterceptor.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.http.interceptor; + + +import com.njzscloud.common.http.support.RequestInfo; +import com.njzscloud.common.http.support.ResponseInfo; + +import java.lang.reflect.Type; + +/** + * 响应拦截器, 在请求之后触发, + * 无论请求是否成功或是发生异常都会调用, + * 可用于响应结果解析 + */ +public interface ResponseInterceptor { + + Object process(RequestInfo requestInfo, ResponseInfo responseInfo, Type responseType); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/BodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/BodyParamProcessor.java new file mode 100644 index 0000000..b784205 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/BodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.BodyParam; + +public interface BodyParamProcessor { + byte[] process(BodyParam bodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultBodyParamProcessor.java new file mode 100644 index 0000000..ef98eb9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultBodyParamProcessor.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.http.processor; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.http.annotation.BodyParam; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +public class DefaultBodyParamProcessor implements BodyParamProcessor { + @Override + public byte[] process(BodyParam bodyParam, String paramName, Class paramClazz, Object paramValue) { + Method toBytesMethod = ReflectUtil.getMethod(paramClazz, "toBytes"); + if (toBytesMethod == null) { + return paramValue.toString().getBytes(StandardCharsets.UTF_8); + } else { + return ReflectUtil.invoke(paramValue, toBytesMethod); + } + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultFormBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultFormBodyParamProcessor.java new file mode 100644 index 0000000..cdc2b76 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultFormBodyParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.FormBodyParam; + +import java.util.List; +import java.util.Map; + +public class DefaultFormBodyParamProcessor implements FormBodyParamProcessor { + @Override + public void process(FormBodyParam formBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultHeaderParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultHeaderParamProcessor.java new file mode 100644 index 0000000..e4b49e2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultHeaderParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.HeaderParam; + +import java.util.List; +import java.util.Map; + +public class DefaultHeaderParamProcessor implements HeaderParamProcessor { + @Override + public void process(HeaderParam headerParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultJsonBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultJsonBodyParamProcessor.java new file mode 100644 index 0000000..7ce19f0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultJsonBodyParamProcessor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.http.annotation.JsonBodyParam; + +public class DefaultJsonBodyParamProcessor implements JsonBodyParamProcessor { + @Override + public byte[] process(JsonBodyParam jsonBodyParam, String paramName, Class paramClazz, Object paramValue) { + return Jackson.toJsonBytes(paramValue); + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultMultiBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultMultiBodyParamProcessor.java new file mode 100644 index 0000000..c104b88 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultMultiBodyParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.http.annotation.MultiBodyParam; + +import java.util.Map; + +public class DefaultMultiBodyParamProcessor implements MultiBodyParamProcessor { + @Override + public void process(MultiBodyParam multiBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultPathParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultPathParamProcessor.java new file mode 100644 index 0000000..aa9cecd --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultPathParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.PathParam; + +import java.util.Map; + +public class DefaultPathParamProcessor implements PathParamProcessor { + + @Override + public void process(PathParam pathParam, String paramName, Class paramClazz, Object paramValue, Map result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultQueryParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultQueryParamProcessor.java new file mode 100644 index 0000000..5aa1494 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultQueryParamProcessor.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.QueryParam; + +import java.util.List; +import java.util.Map; + +public class DefaultQueryParamProcessor implements QueryParamProcessor { + @Override + public void process(QueryParam queryParam, String paramName, Class paramClazz, Object paramValue, Map> result) { + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultXmlBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultXmlBodyParamProcessor.java new file mode 100644 index 0000000..021bf6a --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/DefaultXmlBodyParamProcessor.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.http.annotation.XmlBodyParam; + +public class DefaultXmlBodyParamProcessor implements XmlBodyParamProcessor { + @Override + public byte[] process(XmlBodyParam xmlBodyParam, String paramName, Class paramClazz, Object paramValue) { + return Jackson.toXmlBytes(paramValue); + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/FormBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/FormBodyParamProcessor.java new file mode 100644 index 0000000..41cd6f6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/FormBodyParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.FormBodyParam; + +import java.util.List; +import java.util.Map; + +public interface FormBodyParamProcessor { + void process(FormBodyParam formBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/HeaderParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/HeaderParamProcessor.java new file mode 100644 index 0000000..f39e398 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/HeaderParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.HeaderParam; + +import java.util.List; +import java.util.Map; + +public interface HeaderParamProcessor { + void process(HeaderParam headerParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/JsonBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/JsonBodyParamProcessor.java new file mode 100644 index 0000000..378e38a --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/JsonBodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.JsonBodyParam; + +public interface JsonBodyParamProcessor { + byte[] process(JsonBodyParam jsonBodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/MultiBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/MultiBodyParamProcessor.java new file mode 100644 index 0000000..5975708 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/MultiBodyParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.http.annotation.MultiBodyParam; + +import java.util.Map; + +public interface MultiBodyParamProcessor { + void process(MultiBodyParam multiBodyParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/PathParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/PathParamProcessor.java new file mode 100644 index 0000000..a6c0db2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/PathParamProcessor.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.PathParam; + +import java.util.Map; + +public interface PathParamProcessor { + void process(PathParam pathParam, String paramName, Class paramClazz, Object paramValue, Map result); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/QueryParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/QueryParamProcessor.java new file mode 100644 index 0000000..1c52759 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/QueryParamProcessor.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.QueryParam; + +import java.util.List; +import java.util.Map; + +public interface QueryParamProcessor { + void process(QueryParam queryParam, String paramName, Class paramClazz, Object paramValue, Map> result); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/XmlBodyParamProcessor.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/XmlBodyParamProcessor.java new file mode 100644 index 0000000..00cdd1f --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/processor/XmlBodyParamProcessor.java @@ -0,0 +1,7 @@ +package com.njzscloud.common.http.processor; + +import com.njzscloud.common.http.annotation.XmlBodyParam; + +public interface XmlBodyParamProcessor { + byte[] process(XmlBodyParam xmlBodyParam, String paramName, Class paramClazz, Object paramValue); +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/BodyParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/BodyParamResolver.java new file mode 100644 index 0000000..65b8f9d --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/BodyParamResolver.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.http.annotation.BodyParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.BodyParamProcessor; + +public class BodyParamResolver extends ParamResolver> { + byte[] result = null; + String contentType; + + public BodyParamResolver() { + super(BodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + return Tuple2.of(contentType == null ? Mime.BINARY : contentType, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(BodyParam anno, String paramName, Class paramClazz, Object paramValue) { + contentType = anno.contentType(); + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, BodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + contentType = anno.contentType(); + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/FormBodyParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/FormBodyParamResolver.java new file mode 100644 index 0000000..abfe9f0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/FormBodyParamResolver.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.http.annotation.FormBodyParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.FormBodyParamProcessor; + +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class FormBodyParamResolver extends ParamResolver> { + Map> result = new HashMap<>(); + + public FormBodyParamResolver() { + super(FormBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return null; + StringJoiner joiner = new StringJoiner("&"); + result.forEach((k, v) -> v.forEach(it -> joiner.add(k + "=" + it))); + byte[] bytes = joiner.toString().getBytes(StandardCharsets.UTF_8); + return Tuple2.of(Mime.FORM, bytes); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != FormBodyParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(FormBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/HeaderParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/HeaderParamResolver.java new file mode 100644 index 0000000..b03dc67 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/HeaderParamResolver.java @@ -0,0 +1,92 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.http.annotation.HeaderParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.DefaultHeaderParamProcessor; + +import java.math.RoundingMode; +import java.util.*; + +public class HeaderParamResolver extends ParamResolver> { + Map> result = new TreeMap<>(); + + public HeaderParamResolver() { + super(HeaderParam.class); + } + + @Override + public Map resolve(HttpMethod httpMethod, String urlTpl) { + HashMap map = new HashMap<>(); + result.forEach((k, v) -> map.put(k, String.join(",", v))); + return map; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != DefaultHeaderParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(HeaderParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/JsonBodyParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/JsonBodyParamResolver.java new file mode 100644 index 0000000..f8f1454 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/JsonBodyParamResolver.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.http.annotation.JsonBodyParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.JsonBodyParamProcessor; + +public class JsonBodyParamResolver extends ParamResolver> { + byte[] result = null; + + public JsonBodyParamResolver() { + super(JsonBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + return Tuple2.of(Mime.JSON, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(JsonBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, JsonBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/MultiBodyParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/MultiBodyParamResolver.java new file mode 100644 index 0000000..340b0c6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/MultiBodyParamResolver.java @@ -0,0 +1,141 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.http.annotation.MultiBodyParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.MultiBodyParamProcessor; + +import java.io.ByteArrayOutputStream; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class MultiBodyParamResolver extends ParamResolver> { + private static final byte[] DASHDASH = "--".getBytes(StandardCharsets.UTF_8); + private static final byte[] CRLF = StrUtil.CRLF.getBytes(StandardCharsets.UTF_8); + Map> result = new HashMap<>(); + + public MultiBodyParamResolver() { + super(MultiBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return null; + + String boundary = RandomUtil.randomString(16); + String contentType = Mime.MULTIPART_FORM + "; boundary=" + boundary; + byte[] bytes = buildContent(boundary.getBytes(StandardCharsets.UTF_8)); + return Tuple2.of(contentType, bytes); + } + + private byte[] buildContent(byte[] boundary) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + result.forEach((k, v) -> { + String filename = URLUtil.encode(v.get_0()); + String contentType = v.get_1(); + byte[] bytes = v.get_2(); + + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + IoUtil.write(byteArrayOutputStream, false, boundary); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + byte[] contentDisposition; + if (StrUtil.isNotBlank(filename)) { + contentDisposition = StrUtil.format("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"", k, filename).getBytes(StandardCharsets.UTF_8); + } else { + contentDisposition = StrUtil.format("Content-Disposition: form-data; name=\"{}\"", k).getBytes(StandardCharsets.UTF_8); + } + + IoUtil.write(byteArrayOutputStream, false, contentDisposition); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + if (StrUtil.isNotBlank(contentType)) { + IoUtil.write(byteArrayOutputStream, false, ("Content-Type: " + contentType).getBytes(StandardCharsets.UTF_8)); + IoUtil.write(byteArrayOutputStream, false, CRLF); + } + + IoUtil.write(byteArrayOutputStream, false, CRLF); + IoUtil.write(byteArrayOutputStream, false, bytes); + IoUtil.write(byteArrayOutputStream, false, CRLF); + + }); + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + IoUtil.write(byteArrayOutputStream, false, boundary); + IoUtil.write(byteArrayOutputStream, false, DASHDASH); + + return byteArrayOutputStream.toByteArray(); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz) || isBytes(paramClazz) || isIn(paramClazz) || isFile(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != MultiBodyParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveFormable(MultiBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + byte[] val; + String mime; + String filename = null; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + mime = Mime.TXT; + val = formatNum(format, roundingMode, paramValue).getBytes(StandardCharsets.UTF_8); + } else if (isDt(paramClazz)) { + mime = Mime.TXT; + val = formatDt(format, paramValue).getBytes(StandardCharsets.UTF_8); + } else if (isBytes(paramClazz)) { + mime = Mime.BINARY; + val = (byte[]) paramValue; + } else if (isIn(paramClazz) || isFile(paramClazz)) { + mime = Mime.BINARY; + Tuple2 fileInfo = toFileInfo(paramValue); + filename = fileInfo.get_0(); + val = fileInfo.get_1(); + } else { + mime = Mime.TXT; + val = paramValue.toString().getBytes(StandardCharsets.UTF_8); + } + + result.put(key, Tuple3.of(filename, mime, val)); + + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/ParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/ParamResolver.java new file mode 100644 index 0000000..9898296 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/ParamResolver.java @@ -0,0 +1,242 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.tuple.Tuple3; +import com.njzscloud.common.http.constant.HttpMethod; +import lombok.RequiredArgsConstructor; + +import java.io.File; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +@RequiredArgsConstructor +public abstract class ParamResolver { + protected final Class annoType; + + abstract public R resolve(HttpMethod httpMethod, String urlTpl); + + public final void resolve(Parameter parameter, Object obj) { + if (obj == null) return; + + Class parameterClazz = obj.getClass(); + String parameterName; + T parameterAnno = null; + if (parameter == null) { + parameterName = StrUtil.lowerFirst(obj.getClass().getName()); + } else { + parameterName = parameter.getName(); + parameterAnno = parameter.getAnnotation(this.annoType); + } + + if (parameterAnno == null) { + parameterAnno = parameterClazz.getAnnotation(this.annoType); + } + + int state = 0; + + if (parameterAnno != null) { + state = this.resolveParameter(parameterAnno, parameterName, parameterClazz, obj) ? + 1 : + 2; + } + + if (state == 1) return; + + List getterMethods = ReflectUtil.getPublicMethods(parameterClazz, + it -> (it.getName().startsWith("get") || it.getName().startsWith("is")) && + !Modifier.isStatic(it.getModifiers()) && + !it.getName().equals("getClass") + ); + + for (Method getterMethod : getterMethods) { + Object fieldValue = ReflectUtil.invoke(obj, getterMethod); + if (fieldValue == null) continue; + String getterMethodName = getterMethod.getName(); + Class returnType = getterMethod.getReturnType(); + String fieldName = StrUtil.lowerFirst(getterMethodName.startsWith("get") ? + getterMethodName.substring(3) : + getterMethodName.substring(2)); + + T fieldAnno = getterMethod.getAnnotation(this.annoType); + if (fieldAnno == null) { + Field field = ReflectUtil.getField(parameterClazz, fieldName); + fieldAnno = field.getAnnotation(this.annoType); + } + + if (fieldAnno == null && state == 0) continue; + + this.resolveField(state == 2, fieldAnno, fieldName, returnType, fieldValue); + } + } + + public final void resolve(Object obj) { + resolve(null, obj); + } + + protected abstract boolean resolveParameter(T anno, String paramName, Class paramClazz, Object paramValue); + + protected abstract boolean resolveField(boolean hasParameterAnno, T anno, String paramName, Class paramClazz, Object paramValue); + + + public final boolean isNum(Class clazz) { + return clazz == byte.class || clazz == short.class || + clazz == int.class || clazz == long.class || + clazz == float.class || clazz == double.class + || Number.class.isAssignableFrom(clazz); + } + + protected final boolean isStr(Class clazz) { + return clazz == String.class || clazz == char.class || clazz == Character.class; + } + + protected final boolean isDt(Class clazz) { + return clazz == LocalDate.class || + clazz == LocalTime.class || + clazz == LocalDateTime.class || + Date.class.isAssignableFrom(clazz); + } + + protected final boolean isArr(Class clazz) { + return Collection.class.isAssignableFrom(clazz) || clazz.isArray(); + } + + protected final Collection toArr(Object obj) { + if (obj == null) return null; + Class clazz = obj.getClass(); + if (clazz.isArray()) { + return Arrays.asList((Object[]) obj); + } else if (Collection.class.isAssignableFrom(clazz)) { + return (Collection) obj; + } else { + throw new RuntimeException("不是数组"); + } + } + + protected final boolean isKv(Class clazz) { + return Map.class.isAssignableFrom(clazz); + } + + protected final boolean isIn(Class clazz) { + return InputStream.class.isAssignableFrom(clazz); + } + + protected final boolean isBytes(Class clazz) { + return clazz == byte[].class || clazz == Byte[].class; + } + + protected final Tuple2 toFileInfo(Object obj) { + if (obj == null) return null; + Class clazz = obj.getClass(); + if (isIn(clazz)) { + byte[] bytes = IoUtil.readBytes((InputStream) obj); + return Tuple3.of(null, bytes); + } else if (isFile(clazz)) { + byte[] bytes = FileUtil.readBytes((File) obj); + String name = ((File) obj).getName(); + return Tuple3.of(name, bytes); + } else { + throw new RuntimeException("不是文件"); + } + } + + protected final boolean isFile(Class clazz) { + return File.class.isAssignableFrom(clazz); + } + + + protected final String formatDt(String format, Object value) { + if (value == null) return null; + Class clazz = value.getClass(); + if (Date.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATETIME_PATTERN; + return DateUtil.format(((Date) value), format); + } else if (LocalDateTime.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATETIME_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else if (LocalDate.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_DATE_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else if (LocalTime.class.isAssignableFrom(clazz)) { + if (StrUtil.isBlank(format)) format = DatePattern.NORM_TIME_PATTERN; + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTimeFormatter.format((TemporalAccessor) value); + } else { + throw new RuntimeException("不是时间"); + } + } + + protected final String formatDt(Object value) { + return formatDt(null, value); + } + + protected final String formatNum(String format, RoundingMode roundingMode, Object value) { + if (value == null) return null; + if (StrUtil.isBlank(format)) return formatNum(value); + + Class clazz = value.getClass(); + if (isNum(clazz)) { + return NumberUtil.decimalFormat(format, value, roundingMode); + } else { + throw new RuntimeException("不是数字"); + } + } + + protected final String formatNum(String format, Object value) { + return formatNum(format, null, value); + } + + protected final String formatNum(Object value) { + if (value == null) return null; + + Class clazz = value.getClass(); + if (isNum(clazz)) { + if (clazz == BigDecimal.class) { + return ((BigDecimal) value).toPlainString(); + } else { + return value.toString(); + } + } else { + throw new RuntimeException("不是数字"); + } + } + + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + resolveFormable(null, k.toString(), v.getClass(), v); + }); + } + + protected void resolveArr(String paramName, Object paramValue) { + toArr(paramValue) + .stream() + .filter(Objects::nonNull) + .forEach(it -> resolveFormable(null, paramName, it.getClass(), it)); + } + + protected void resolveFormable(T anno, String paramName, Class paramClazz, Object paramValue) { + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/PathParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/PathParamResolver.java new file mode 100644 index 0000000..49e74d9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/PathParamResolver.java @@ -0,0 +1,74 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.http.annotation.PathParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.PathParamProcessor; + +import java.math.RoundingMode; +import java.util.Map; +import java.util.TreeMap; + +public class PathParamResolver extends ParamResolver { + Map result = new TreeMap<>(); + + public PathParamResolver() { + super(PathParam.class); + } + + @Override + public String resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return urlTpl; + return StrUtil.format(urlTpl, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(PathParam anno, String paramName, Class paramClazz, Object paramValue) { + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, PathParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != PathParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveFormable(PathParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.put(key, val); + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/QueryParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/QueryParamResolver.java new file mode 100644 index 0000000..1203c87 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/QueryParamResolver.java @@ -0,0 +1,92 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.http.annotation.QueryParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.DefaultQueryParamProcessor; + +import java.math.RoundingMode; +import java.util.*; + +public class QueryParamResolver extends ParamResolver { + Map> result = new TreeMap<>(); + + public QueryParamResolver() { + super(QueryParam.class); + } + + @Override + public String resolve(HttpMethod httpMethod, String urlTpl) { + if (result.isEmpty()) return urlTpl; + StringJoiner joiner = new StringJoiner("&", urlTpl + (urlTpl.contains("?") ? "&" : "?"), ""); + result.forEach((k, v) -> v.forEach(it -> joiner.add(k + "=" + it))); + return joiner.toString(); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + String value = anno.value(); + if (isKv(paramClazz)) resolveKv(paramValue); + else if (isArr(paramClazz)) resolveArr(StrUtil.isBlank(value) ? paramName : value, paramValue); + else if (isStr(paramClazz) || isNum(paramClazz) || isDt(paramClazz)) resolveFormable(anno, paramName, paramClazz, paramValue); + else return false; + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + if (anno != null) { + Class processorClazz = anno.processor(); + if (processorClazz != DefaultQueryParamProcessor.class) { + ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue, result); + } else if (!hasParameterAnno && isKv(paramClazz)) { + resolveKv(paramValue); + } + } + if (anno != null) { + String value = anno.value(); + if (StrUtil.isNotBlank(value)) paramName = value; + } + if (isArr(paramClazz)) resolveArr(paramName, paramValue); + else resolveFormable(anno, paramName, paramClazz, paramValue); + return true; + } + + protected void resolveKv(Object paramValue) { + ((Map) paramValue).forEach((k, v) -> { + if (v == null) return; + Class clazz = v.getClass(); + String paramName = k.toString(); + if (isArr(clazz)) resolveArr(paramName, v); + else resolveFormable(null, paramName, clazz, v); + }); + } + + + protected void resolveFormable(QueryParam anno, String paramName, Class paramClazz, Object paramValue) { + String key = paramName; + String val; + + String format = null; + RoundingMode roundingMode = null; + if (anno != null) { + format = anno.format(); + roundingMode = anno.roundingMode(); + String value = anno.value(); + if (StrUtil.isNotBlank(value)) key = value; + } + + if (isNum(paramClazz)) { + val = formatNum(format, roundingMode, paramValue); + } else if (isDt(paramClazz)) { + val = formatDt(format, paramValue); + } else { + val = paramValue.toString(); + } + result.computeIfAbsent(key, it -> new ArrayList<>()).add(val); + } + +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/XmlBodyParamResolver.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/XmlBodyParamResolver.java new file mode 100644 index 0000000..c717318 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/resolver/XmlBodyParamResolver.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.http.resolver; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.http.annotation.XmlBodyParam; +import com.njzscloud.common.http.constant.HttpMethod; +import com.njzscloud.common.http.processor.XmlBodyParamProcessor; + +public class XmlBodyParamResolver extends ParamResolver> { + byte[] result = null; + + public XmlBodyParamResolver() { + super(XmlBodyParam.class); + } + + @Override + public Tuple2 resolve(HttpMethod httpMethod, String urlTpl) { + if (result == null) return null; + + return Tuple2.of(Mime.XML, result); + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveParameter(XmlBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean resolveField(boolean hasParameterAnno, XmlBodyParam anno, String paramName, Class paramClazz, Object paramValue) { + if (hasParameterAnno) return false; + + Class processorClazz = anno.processor(); + result = ReflectUtil.newInstance(processorClazz).process(anno, paramName, paramClazz, paramValue); + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ParameterInfo.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ParameterInfo.java new file mode 100644 index 0000000..e05bed1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ParameterInfo.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.http.support; + +import java.lang.annotation.Annotation; +import java.util.List; + +public class ParameterInfo { + private String name; + private List annoList; + private Object value; +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/RequestInfo.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/RequestInfo.java new file mode 100644 index 0000000..bcbc80e --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/RequestInfo.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.http.support; + +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.http.constant.HttpMethod; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class RequestInfo { + public final String desc; + public final HttpMethod method; + public final String url; + public final Map headers; + public final String contentType; + public final byte[] body; + + public static RequestInfo create(String desc, HttpMethod method, String url, Map headers, Tuple2 body) { + String contentType = null; + byte[] bytes = null; + if (body != null) { + contentType = body.get_0(); + bytes = body.get_1(); + } + return new RequestInfo(desc, method, url, MapUtil.unmodifiable(headers), contentType, bytes); + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseInfo.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseInfo.java new file mode 100644 index 0000000..a311c63 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseInfo.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.http.support; + +import cn.hutool.core.map.MapUtil; + +import java.util.List; +import java.util.Map; + +public record ResponseInfo(boolean success, int code, String message, Map> header, byte[] body, Exception e) { + + public static ResponseInfo create(int code, String message, Map> header, byte[] body) { + return new ResponseInfo(code >= 200 && code <= 299, code, message, header, body, null); + } + + public static ResponseInfo create(Exception e) { + return new ResponseInfo(false, 0, e.getMessage(), MapUtil.empty(), new byte[0], e); + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseResult.java b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseResult.java new file mode 100644 index 0000000..d6d38fa --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/java/com/njzscloud/common/http/support/ResponseResult.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.http.support; + +import java.util.List; +import java.util.Map; + +public final class ResponseResult { + /** + * 是否成功, HTTP 状态码 200..299 + */ + public final boolean success; + + /** + * HTTP 状态码 + */ + public final int code; + + /** + * HTTP 状态信息 + */ + public final String status; + + /** + * HTTP 响应头 + */ + public final Map> headers; + + /** + * HTTP 响应体 + */ + public final T body; + + /** + * 错误信息, 在请求发生异常时有值 + */ + public final Exception e; + + private ResponseResult(boolean success, int code, String status, Map> headers, T body, Exception e) { + this.success = success; + this.code = code; + this.status = status; + this.headers = headers; + this.body = body; + this.e = e; + } + + public static ResponseResultBuilder builder() { + return new ResponseResultBuilder(); + } + + public static class ResponseResultBuilder { + private int code; + private String status; + private Map> headers; + private T body; + private Exception e; + + private ResponseResultBuilder() { + } + + public ResponseResultBuilder code(int code) { + this.code = code; + return this; + } + + public ResponseResultBuilder status(String status) { + this.status = status; + return this; + } + + public ResponseResultBuilder headers(Map> headers) { + this.headers = headers; + return this; + } + + public ResponseResultBuilder body(T body) { + this.body = body; + return this; + } + + public ResponseResultBuilder e(Exception e) { + this.e = e; + return this; + } + + public ResponseResult build() { + return new ResponseResult<>( + code >= 200 && code <= 299, + code, + status, + headers, + body, + e + ); + } + + } +} diff --git a/njzscloud-common/njzscloud-common-http/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-http/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..a1f4eb6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-http/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.http.config.HttpClientAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-mp/pom.xml b/njzscloud-common/njzscloud-common-mp/pom.xml new file mode 100644 index 0000000..2112770 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-mp + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-security + provided + + + com.jcraft + jsch + 0.1.55 + + + + com.mysql + mysql-connector-j + + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.baomidou + mybatis-plus-jsqlparser + + + + com.baomidou + mybatis-plus-jsqlparser-4.9 + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java new file mode 100644 index 0000000..45aee66 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MpAutoConfiguration.java @@ -0,0 +1,40 @@ +package com.njzscloud.common.mp.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.njzscloud.common.mp.support.MetaObjectHandlerImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Mybatis-Plus 配置 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({MybatisPlusProperties.class}) +public class MpAutoConfiguration { + + /** + * 插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisPlusProperties mybatisPlusProperties) { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + // 分页 + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + // 乐观锁 + // mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + // 防止全表更新删除 + mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + + // DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor(new CustomDataPermissionHandler(mybatisPlusProperties.getIgnoreTables())); + // mybatisPlusInterceptor.addInnerInterceptor(dataPermissionInterceptor); + return mybatisPlusInterceptor; + } + + @Bean + public MetaObjectHandlerImpl metaObjectHandlerImpl() { + return new MetaObjectHandlerImpl(); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java new file mode 100644 index 0000000..7ab76c2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/config/MybatisPlusProperties.java @@ -0,0 +1,14 @@ +package com.njzscloud.common.mp.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Getter +@Setter +@ConfigurationProperties("mybatis-plus") +public class MybatisPlusProperties { + private List ignoreTables; +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java new file mode 100644 index 0000000..deadbbf --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/CustomDataPermissionHandler.java @@ -0,0 +1,42 @@ +package com.njzscloud.common.mp.support; + +import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; + +import java.util.List; +import java.util.function.Supplier; + +@Slf4j +@AllArgsConstructor +public class CustomDataPermissionHandler implements MultiDataPermissionHandler { + + public static final Supplier TENANT_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long tenantId = 0L; + if (userDetail != null) tenantId = userDetail.getTenantId(); + if (tenantId == null) tenantId = 0L; + return tenantId; + }; + private final List ignoreTables; + + @Override + public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + Long tenantId = TENANT_ID_SUPPLIER.get(); + String tableName = table.getName(); + if ((ignoreTables != null && ignoreTables.contains(tableName)) || tenantId == null || tenantId == 0L) + return null; + try { + return CCJSqlParserUtil.parseCondExpression("tenant_id = " + tenantId); + } catch (JSQLParserException e) { + log.error("租户 Id 条件设置失败:{}", tenantId, e); + return null; + } + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java new file mode 100644 index 0000000..e2fc4d9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/MetaObjectHandlerImpl.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.mp.support; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; +import java.util.function.Supplier; + +/** + * 字段填充 + * 填充字段:createTime、creatorId、modifyTime、modifierId + */ +public class MetaObjectHandlerImpl implements MetaObjectHandler { + /** + * 获取当前登录人 Id,默认:0 + */ + public static final Supplier OPERATOR_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long userId = 0L; + if (userDetail != null) userId = userDetail.getUserId(); + if (userId == null) userId = 0L; + return userId; + }; + public static final Supplier TENANT_ID_SUPPLIER = () -> { + UserDetail userDetail = SecurityUtil.loginUser(); + Long tenantId = 0L; + if (userDetail != null) tenantId = userDetail.getTenantId(); + if (tenantId == null) tenantId = 0L; + return tenantId; + }; + + public static final Supplier OPERATOR_TIME_SUPPLIER = LocalDateTime::now; + + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictInsertFill(metaObject, "creatorId", OPERATOR_ID_SUPPLIER, Long.class); + + this.strictInsertFill(metaObject, "modifyTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictInsertFill(metaObject, "modifierId", OPERATOR_ID_SUPPLIER, Long.class); + this.strictInsertFill(metaObject, "tenantId", TENANT_ID_SUPPLIER, Long.class); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "modifyTime", OPERATOR_TIME_SUPPLIER, LocalDateTime.class); + this.strictUpdateFill(metaObject, "modifierId", OPERATOR_ID_SUPPLIER, Long.class); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java new file mode 100644 index 0000000..7680965 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageParam.java @@ -0,0 +1,65 @@ +package com.njzscloud.common.mp.support; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/** + * 分页参数 + */ +@Getter +@Setter +@Accessors(chain = true) +public final class PageParam { + /** + * 每页显示条数,默认 10 + */ + private Integer size; + + /** + * 当前页 + */ + private Integer current; + + /** + * 排序字段信息 + * 格式:a:desc,b:asc + */ + private String orders; + + public Page toPage() { + List orderItemList = new ArrayList<>(); + if (StrUtil.isNotBlank(orders)) { + String[] orderItems = orders.split(","); + for (String orderItem : orderItems) { + String[] item = orderItem.split(":"); + OrderItem orderItem_ = new OrderItem(); + if (item.length == 1) { + orderItem_.setColumn(item[0]); + orderItemList.add(orderItem_); + } else if (item.length == 2) { + orderItem_.setColumn(item[0]); + if ("asc".equalsIgnoreCase(item[1])) { + orderItem_.setAsc(true); + } else if ("desc".equalsIgnoreCase(item[1])) { + orderItem_.setAsc(false); + } else { + throw new RuntimeException(StrUtil.format("排序参数错误,字段:orders,接收到的值:{}", orders)); + } + orderItemList.add(orderItem_); + } else { + throw new RuntimeException(StrUtil.format("排序参数错误,字段:orders,接收到的值:{}", orders)); + } + } + } + Page page = new Page<>(current == null ? 1 : current, size == null ? 500 : size); + page.setOrders(orderItemList); + return page; + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java new file mode 100644 index 0000000..3b9eec2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/PageResult.java @@ -0,0 +1,56 @@ +package com.njzscloud.common.mp.support; + +import cn.hutool.core.collection.ListUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.Collections; +import java.util.List; + +/** + * 分页结果类 + */ +@Getter +@Setter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class PageResult { + + private final List records; + + /** + * 总数 + */ + private final Integer total; + + /** + * 总页数 + */ + private final Integer pages; + + /** + * 当前页 + */ + private final Integer current; + + /** + * 每页显示条数,默认 10 + */ + private final Integer size; + + + public static PageResult of(IPage page) { + return new PageResult<>(page.getRecords(), (int) page.getTotal(), (int) page.getPages(), (int) page.getCurrent(), (int) page.getSize()); + } + + public static PageResult of(int current, int size, List data) { + if (data == null) data = Collections.emptyList(); + int total = data.size(); + int pages = Math.toIntExact(total % size == 0 ? (total / size) : (total / size + 1)); + + return new PageResult<>(ListUtil.page(current, size, data), total, pages, current, size); + } + +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java new file mode 100644 index 0000000..85c7b83 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/e/EnumTypeHandlerDealer.java @@ -0,0 +1,123 @@ +package com.njzscloud.common.mp.support.handler.e; + +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.EnumTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 枚举类型处理器
+ * 仅处理 Dict 类型的枚举, 其他枚举类型采用 Mybatis 默认处理器
+ * 使用配置项 mybatis-plus.configuration.default-enum-type-handler + * + * @see Dict + * @see DictInt + * @see DictStr + */ +@Slf4j +public class EnumTypeHandlerDealer> extends EnumTypeHandler { + + private final Object[] enums; + private final EnumType enumType; + + /** + * 构建枚举类型处理器 + * + * @param type 枚举类型 + * @see DictInt + * @see DictStr + */ + public EnumTypeHandlerDealer(Class type) { + super(type); + + enums = type.getEnumConstants(); + + if (DictInt.class.isAssignableFrom(type)) { + enumType = EnumType.DICT_INT; + } else if (DictStr.class.isAssignableFrom(type)) { + enumType = EnumType.DICT_STR; + } else { + enumType = EnumType.OTHER; + log.warn("枚举类型 [{}] 未实现 [{}] 或 [{}], 数据库操作将使用 Mybatis 默认处理器", type, DictInt.class.getName(), DictStr.class.getName()); + } + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { + switch (enumType) { + case OTHER: + super.setNonNullParameter(ps, i, parameter, jdbcType); + break; + case DICT_INT: + ps.setObject(i, ((DictInt) parameter).getVal()); + break; + case DICT_STR: + ps.setObject(i, ((DictStr) parameter).getVal()); + break; + } + } + + @Override + public E getNullableResult(ResultSet rs, String columnName) throws SQLException { + String strVal = rs.getString(columnName); + + if (strVal == null) { + return null; + } + + if (enumType == EnumType.OTHER) { + return super.getNullableResult(rs, columnName); + } + return getNullableResult(strVal); + } + + @Override + public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String strVal = rs.getString(columnIndex); + if (strVal == null) { + return null; + } + + if (enumType == EnumType.OTHER) { + return super.getNullableResult(rs, columnIndex); + } + return getNullableResult(strVal); + } + + @Override + public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String strVal = cs.getString(columnIndex); + + if (strVal == null) { + return null; + } + if (enumType == EnumType.OTHER) { + return super.getNullableResult(cs, columnIndex); + } + + return getNullableResult(strVal); + } + + @SuppressWarnings("unchecked") + private E getNullableResult(String strVal) { + if (enumType == EnumType.DICT_INT) { + int val = Integer.parseInt(strVal); + return (E) Dict.parse(val, (DictInt[]) enums); + } else { + return (E) Dict.parse(strVal, (DictStr[]) enums); + } + } + + private enum EnumType { + OTHER, + DICT_INT, + DICT_STR + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java new file mode 100644 index 0000000..80c1204 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/java/com/njzscloud/common/mp/support/handler/j/JsonTypeHandler.java @@ -0,0 +1,57 @@ +package com.njzscloud.common.mp.support.handler.j; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.fastjson.Fastjson; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Json 类型处理器 + */ +@Slf4j +@MappedTypes({Object.class}) +@MappedJdbcTypes({JdbcType.VARCHAR}) +public class JsonTypeHandler implements TypeHandler { + + private static Object getResult(String result) { + if (StrUtil.isBlank(result)) return null; + try { + return Fastjson.toBean(result, Object.class); + } catch (Exception e) { + return result; + } + } + + @Override + public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { + if (parameter == null) ps.setNull(i, jdbcType.TYPE_CODE); + else ps.setString(i, Fastjson.toJsonStr(parameter)); + + } + + @Override + public Object getResult(ResultSet rs, String columnName) throws SQLException { + String result = rs.getString(columnName); + return getResult(result); + } + + @Override + public Object getResult(ResultSet rs, int columnIndex) throws SQLException { + String result = rs.getString(columnIndex); + return getResult(result); + } + + @Override + public Object getResult(CallableStatement cs, int columnIndex) throws SQLException { + String result = cs.getString(columnIndex); + return getResult(result); + } +} diff --git a/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..6ef84a0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.mp.config.MpAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-mqtt/pom.xml b/njzscloud-common/njzscloud-common-mqtt/pom.xml new file mode 100644 index 0000000..0625f11 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-mqtt + jar + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + + diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttAutoConfiguration.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttAutoConfiguration.java new file mode 100644 index 0000000..b4d1030 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.njzscloud.common.mqtt.config; + +import com.njzscloud.common.mqtt.support.MqttCliWrapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnBooleanProperty(value = "mqtt.enabled") +@EnableConfigurationProperties(MqttProperties.class) +public class MqttAutoConfiguration { + + @Bean(destroyMethod = "shutdown", name = "mqtt") + public MqttCliWrapper mqtt(MqttProperties mqttProperties) { + return new MqttCliWrapper(mqttProperties); + } +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttProperties.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttProperties.java new file mode 100644 index 0000000..28efc84 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/config/MqttProperties.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.mqtt.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties("mqtt") +public class MqttProperties { + private boolean enabled = false; + private String broker; + private String clientId; + private String username; + private String password; + private String logDir = "logs/mqtt"; +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttCliWrapper.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttCliWrapper.java new file mode 100644 index 0000000..7e86fbb --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttCliWrapper.java @@ -0,0 +1,156 @@ +package com.njzscloud.common.mqtt.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.thread.ThreadPool; +import com.njzscloud.common.mqtt.config.MqttProperties; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.EventListener; + +import java.lang.reflect.Method; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Consumer; + +@Slf4j +public class MqttCliWrapper { + static final Map> fn = new ConcurrentHashMap<>(); + private final MqttClient client; + private final ThreadPoolExecutor threadPool = ThreadPool.createThreadPool("MQTT 处理器"); + + public MqttCliWrapper(MqttProperties mqttProperties) { + log.info("mqtt 客户端已启用"); + String broker = mqttProperties.getBroker(); + String clientId = mqttProperties.getClientId() + "_" + new Date().getTime(); + String username = mqttProperties.getUsername(); + String password = mqttProperties.getPassword(); + String logDir = mqttProperties.getLogDir(); + try { + MqttDefaultFilePersistence persistence = new MqttDefaultFilePersistence(logDir); + client = new MqttClient(broker, clientId, persistence); + MqttConnectOptions options = new MqttConnectOptions(); + options.setAutomaticReconnect(true); + options.setConnectionTimeout(5000); + options.setMaxReconnectDelay(30000); + if (StrUtil.isNotBlank(username) && StrUtil.isNotBlank(password)) { + options.setUserName(username); + options.setPassword(password.toCharArray()); + } + client.connect(options); + client.setCallback(new MsgHandler(fn, threadPool)); + } catch (Exception e) { + throw Exceptions.error(e); + } + } + + public void shutdown() { + try { + if (client != null) { + client.disconnect(); + } + threadPool.shutdown(); + } catch (Exception e) { + log.error("mqtt关闭失败", e); + } + } + + public void subscribe(String topic, int qos) { + try { + client.subscribe(topic, qos); + log.info("mqtt 订阅消息:{} {}", topic, qos); + } catch (Exception e) { + log.error("mqtt 订阅失败:{} {}", topic, qos, e); + } + } + + public void subscribe(String topic) { + this.subscribe(topic, 0); + } + + public void subscribe(String topic, int qos, Consumer handler) { + fn.put(topic, handler); + this.subscribe(topic, qos); + } + + public void subscribe(String topic, Consumer handler) { + this.subscribe(topic, 0, handler); + } + + public void publish(String topic, int qos, Object msg) { + String jsonStr = null; + try { + jsonStr = Jackson.toJsonStr(msg); + // log.info("mqtt 发布消息:{} {} {}", topic, qos, jsonStr); + MqttMessage message = new MqttMessage(jsonStr.getBytes()); + message.setQos(qos); + client.publish(topic, message); + } catch (Exception e) { + log.error("mqtt 发布消息失败:{} {} {}", topic, qos, jsonStr, e); + } + } + + public void publish(String topic, Object msg) { + this.publish(topic, 0, msg); + } + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReadyEvent() { + ApplicationContext applicationContext = SpringUtil.getApplicationContext(); + String[] beanNames = applicationContext.getBeanDefinitionNames(); + for (String beanName : beanNames) { + Object bean = applicationContext.getBean(beanName); + process(bean); + } + } + + public void process(Object bean) throws BeansException { + Class clazz = bean.getClass(); + Method[] methods; + boolean proxy = AopUtils.isCglibProxy(bean); + if (proxy) { + Class parentClazz = clazz.getSuperclass(); + methods = parentClazz.getMethods(); + } else { + methods = clazz.getMethods(); + } + for (Method method : methods) { + MqttListener mqttListener = method.getAnnotation(MqttListener.class); + if (mqttListener == null) continue; + if (proxy) { + try { + String methodName = method.getName(); + method = clazz.getMethod(methodName, method.getParameterTypes()); + } catch (NoSuchMethodException e) { + continue; + } + } + String topic = mqttListener.topic(); + int qos = mqttListener.qos(); + this.subscribe(topic, qos); + Consumer handler = new MsgConsumer(bean, method); + fn.put(topic, handler); + } + } + + public void unsubscribe(String topic) { + try { + client.unsubscribe(topic); + } catch (Exception e) { + log.error("mqtt 取消订阅失败:{}", topic, e); + } + } + + +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttListener.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttListener.java new file mode 100644 index 0000000..916fe21 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttListener.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.mqtt.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 将函数标记为 MQTT 消息处理函数,然后将类注册为 Spring Bean 即可 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface MqttListener { + String topic(); + + int qos() default 0; +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttMsg.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttMsg.java new file mode 100644 index 0000000..1452f7c --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MqttMsg.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.mqtt.support; + +import com.njzscloud.common.core.jackson.Jackson; + +import java.lang.reflect.Type; + +public class MqttMsg { + private final byte[] msg; + + public MqttMsg(byte[] msg) { + this.msg = msg; + } + + public T getMsg(Class clazz) { + return Jackson.toBean(msg, clazz); + } + + public T getMsg(Type type) { + return Jackson.toBean(this.msg, type); + } +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgConsumer.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgConsumer.java new file mode 100644 index 0000000..f107e87 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgConsumer.java @@ -0,0 +1,18 @@ +package com.njzscloud.common.mqtt.support; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +@RequiredArgsConstructor +public class MsgConsumer implements Consumer { + private final Object object; + private final Method method; + + @SneakyThrows + public void accept(MqttMsg msg) { + method.invoke(object, msg); + } +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgHandler.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgHandler.java new file mode 100644 index 0000000..87b210e --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/support/MsgHandler.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.mqtt.support; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Consumer; + +@Slf4j +public class MsgHandler implements MqttCallback { + private final Map> fn; + private final ThreadPoolExecutor threadPool; + + public MsgHandler(Map> fn, ThreadPoolExecutor threadPool) { + this.threadPool = threadPool; + this.fn = fn; + } + + public void messageArrived(String topic, MqttMessage message) throws Exception { + Consumer handler = fn.get(topic); + if (handler == null) return; + try { + CompletableFuture + .runAsync(() -> handler.accept(new MqttMsg(message.getPayload())), threadPool) + .exceptionally(e -> { + log.error("mqtt 消息处理失败:{} {}", topic, new String(message.getPayload()), e); + return null; + }); + } catch (Exception e) { + log.error("mqtt 消息处理失败", e); + } + } + + public void connectionLost(Throwable cause) { + log.error("mqtt 连接已断开", cause); + } + + public void deliveryComplete(IMqttDeliveryToken token) { + // log.info("消息投递结果:{}", token.isComplete()); + } +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/util/Mqtt.java b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/util/Mqtt.java new file mode 100644 index 0000000..cc125c9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/java/com/njzscloud/common/mqtt/util/Mqtt.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.mqtt.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.mqtt.support.MqttCliWrapper; +import com.njzscloud.common.mqtt.support.MqttMsg; + +import java.util.function.Consumer; + +public final class Mqtt { + private static final MqttCliWrapper MQTT_CLI_WRAPPER; + + static { + MQTT_CLI_WRAPPER = SpringUtil.getBean(MqttCliWrapper.class); + } + + public static void subscribe(String topic, int qos, Consumer handler) { + MQTT_CLI_WRAPPER.subscribe(topic, qos, handler); + } + + public static void subscribe(String topic, Consumer handler) { + MQTT_CLI_WRAPPER.subscribe(topic, handler); + } + + public static void subscribe(String topic, int qos) { + MQTT_CLI_WRAPPER.subscribe(topic, qos); + } + + public static void subscribe(String topic) { + MQTT_CLI_WRAPPER.subscribe(topic); + } + + + public static void publish(String topic, Object msg) { + MQTT_CLI_WRAPPER.publish(topic, msg); + } + + public static void publish(String topic) { + MQTT_CLI_WRAPPER.publish(topic, MapUtil.builder() + .put("topic", topic) + .build()); + } + + public static void publish(String topic, int qos, Object msg) { + MQTT_CLI_WRAPPER.publish(topic, qos, msg); + } + + public static void unsubscribe(String topic) { + MQTT_CLI_WRAPPER.unsubscribe(topic); + } +} diff --git a/njzscloud-common/njzscloud-common-mqtt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-mqtt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..5620b2f --- /dev/null +++ b/njzscloud-common/njzscloud-common-mqtt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.mqtt.config.MqttAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-mvc/pom.xml b/njzscloud-common/njzscloud-common-mvc/pom.xml new file mode 100644 index 0000000..2b1b14e --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-mvc + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + + jakarta.validation + jakarta.validation-api + + + + org.hibernate.validator + hibernate-validator + + + + + jakarta.servlet + jakarta.servlet-api + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-configuration-processor + + + diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java new file mode 100644 index 0000000..2e9ab0d --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/MvcAutoConfiguration.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.mvc.config; + +import com.njzscloud.common.core.jackson.serializer.BigDecimalModule; +import com.njzscloud.common.core.jackson.serializer.LongModule; +import com.njzscloud.common.core.jackson.serializer.TimeModule; +import com.njzscloud.common.mvc.support.DictHandlerMethodArgumentResolver; +import com.njzscloud.common.mvc.support.GlobalExceptionController; +import com.njzscloud.common.mvc.support.ReusableRequestFilter; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Spring MVC 配置 + */ +@Configuration(proxyBeanMethods = false) +public class MvcAutoConfiguration { + /** + * Jackson 配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return build -> build + .modules(new TimeModule(), new LongModule(), new BigDecimalModule()); + } + + @Bean + public DictHandlerMethodArgumentResolver dictHandlerMethodArgumentResolver() { + return new DictHandlerMethodArgumentResolver(); + } + + @Bean + public GlobalExceptionController globalExceptionController() { + return new GlobalExceptionController(); + } + + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ReusableRequestFilter()); + registration.addUrlPatterns("/*"); + registration.setName("reusableRequestFilter"); + registration.setOrder(0); + return registration; + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java new file mode 100644 index 0000000..a740461 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/config/RequestMappingHandlerAdapterAutoConfiguration.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.mvc.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 参数、返回值处理器配置 + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +public class RequestMappingHandlerAdapterAutoConfiguration implements InitializingBean { + + private final RequestMappingHandlerAdapter requestMappingHandlerAdapter; + + /** + * 收集自定义的 Controller 方法参数处理器 + */ + private final ObjectProvider handlerMethodArgumentResolver; + + /** + * 收集自定义的 Controller 方法返回值处理器 + */ + private final ObjectProvider handlerMethodReturnValueHandler; + + public RequestMappingHandlerAdapterAutoConfiguration(@Autowired(required = false) RequestMappingHandlerAdapter requestMappingHandlerAdapter, + ObjectProvider handlerMethodArgumentResolver, + ObjectProvider handlerMethodReturnValueHandler) { + this.requestMappingHandlerAdapter = requestMappingHandlerAdapter; + this.handlerMethodArgumentResolver = handlerMethodArgumentResolver; + this.handlerMethodReturnValueHandler = handlerMethodReturnValueHandler; + } + + @Override + public void afterPropertiesSet() throws Exception { + + if (requestMappingHandlerAdapter == null) { + log.warn("RequestMappingHandlerAdapter 为空"); + return; + } + + List handlerMethodArgumentResolverList = handlerMethodArgumentResolver.orderedStream().collect(Collectors.toList()); + List handlerMethodReturnValueHandlerList = handlerMethodReturnValueHandler.orderedStream().collect(Collectors.toList()); + + // Controller 方法参数处理器 + if (CollUtil.isNotEmpty(handlerMethodArgumentResolverList)) { + List argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers(); + if (argumentResolvers != null) { + handlerMethodArgumentResolverList.addAll(argumentResolvers); + } + requestMappingHandlerAdapter.setArgumentResolvers(handlerMethodArgumentResolverList); + } + + // Controller 方法返回值处理器 + if (CollUtil.isNotEmpty(handlerMethodReturnValueHandlerList)) { + List returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers(); + if (returnValueHandlers != null) { + handlerMethodReturnValueHandlerList.addAll(returnValueHandlers); + } + requestMappingHandlerAdapter.setReturnValueHandlers(handlerMethodReturnValueHandlerList); + } + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java new file mode 100644 index 0000000..16ac323 --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/DictHandlerMethodArgumentResolver.java @@ -0,0 +1,60 @@ +package com.njzscloud.common.mvc.support; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.core.ienum.DictInt; +import com.njzscloud.common.core.ienum.DictStr; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * 字典枚举参数处理器 + */ +@SuppressWarnings({"ConstantConditions"}) +public class DictHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * 支持 {@link DictInt} 和 {@link DictStr} 类型的返回值 + * + * @param parameter 返回值信息 + * @return 当参数类型为 DictInt 或 DictStr 时返回 true + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return DictInt.class.isAssignableFrom(parameter.getParameterType()) || DictStr.class.isAssignableFrom(parameter.getParameterType()); + } + + /** + * 解析参数 + * + * @param parameter 参数信息 + * @param container 当前请求的模型视图容器 + * @param request 当前请求对象 + * @param factory 创建 {@link WebDataBinder} 的工厂对象 + * @return 返回 {@link DictInt} 或 {@link DictStr} 对象 + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception { + String parameterName = parameter.getParameterName(); + String param = request.getParameter(parameterName); + + if (StrUtil.isBlank(param)) return null; + + Class type = parameter.getParameterType(); + + if (DictInt.class.isAssignableFrom(type)) { + Integer val = Integer.parseInt(param); + DictInt[] constants = (DictInt[]) type.getEnumConstants(); + return Dict.parse(val, constants); + } else if (DictStr.class.isAssignableFrom(type)) { + DictStr[] constants = (DictStr[]) type.getEnumConstants(); + return Dict.parse(param, constants); + } else { + return null; + } + } +} diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java new file mode 100644 index 0000000..aa65c9a --- /dev/null +++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/GlobalExceptionController.java @@ -0,0 +1,323 @@ +package com.njzscloud.common.mvc.support; + +import com.njzscloud.common.core.ex.CliException; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.SysError; +import com.njzscloud.common.core.ex.SysException; +import com.njzscloud.common.core.utils.R; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.validation.BindException; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 异常处理器 + * + * @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionController { + + /** + * 请求方法不匹配 + */ + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public R httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) { + printRequest(); + String method = e.getMethod(); + String[] supportedMethods = e.getSupportedMethods(); + log.error("不支持的请求方法:【{}】仅支持:【{}】", method, supportedMethods, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "不支持的请求方法:【{}】仅支持:【{}】", method, supportedMethods); + } + + /** + * 内容类型不匹配 + */ + @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public R httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) { + printRequest(); + MediaType contentType = e.getContentType(); + List supportedMediaTypes = e.getSupportedMediaTypes(); + log.error("不支持的内容类型:【{}】仅支持:【{}】", contentType, supportedMediaTypes, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "不支持的内容类型:【{}】仅支持:【{}】", contentType, supportedMediaTypes); + } + + /** + * 未发现多部分表单 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestPartException.class) + public R missingServletRequestPartExceptionHandler(MissingServletRequestPartException e) { + printRequest(); + String requestPartName = e.getRequestPartName(); + log.error("未发现多部分表单, 字段:【{}】", requestPartName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "未发现多部分表单, 字段:【{}】", requestPartName); + } + + /** + * 缺少必要参数, 使用 @RequestParam(required = true) 的方法出现的异常 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException.class) + public R missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) { + printRequest(); + String parameterName = e.getParameterName(); + log.error("缺少必要参数, 字段:【{}】必须有值", parameterName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要参数, 字段:【{}】必须有值", parameterName); + } + + /** + * 缺少必要参数, 使用 @RequestHeader(required = true) 的方法出现的异常 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingRequestHeaderException.class) + public R missingRequestHeaderExceptionHandler(MissingRequestHeaderException e) { + printRequest(); + String headerName = e.getHeaderName(); + log.error("缺少必要的请求头参数, 请求头:【{}】必须有值", headerName, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要的请求头参数, 请求头:【{}】必须有值", headerName); + } + + /** + * 缺少必要的请求参数 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(ServletRequestBindingException.class) + public R servletRequestBindingExceptionHandler(ServletRequestBindingException e) { + printRequest(); + log.error("缺少必要的请求参数", e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "缺少必要参数,【{}】", e.getMessage()); + } + + /** + * 方法参数类型与给定的参数类型不匹配 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public R methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) { + printRequest(); + String name = e.getName(); + Object value = e.getValue(); + Class requiredType = e.getRequiredType(); + String simpleName = null; + if (requiredType != null) { + simpleName = requiredType.getSimpleName(); + } + log.error("参数类型不匹配, 字段:【{}】需要的类型为:【{}】, 接收到的值为:【{}】", name, simpleName, value, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "参数类型不匹配, 字段:【{}】需要的类型为:【{}】, 接收到的值为:【{}】", name, simpleName, value); + } + + + /** + * 读取请求参数失败,可能为参数格式错误,POST 请求发送 JSON 数据, 数据类型不匹配、时间格式不正确等 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public R httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) { + printRequest(); + log.error("读取请求体参数失败, 可能是参数格式错误", e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, "读取请求体参数失败, 可能是参数格式错误.【{}】", e.getMessage()); + } + + /** + * 注解 @RequestBody 和 @Validated 标注的对象,校验失败 + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public R methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + printRequest(); + List allErrors = e.getAllErrors(); + List msg = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()); + log.error("参数校验失败:【{}】", msg, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, msg); + } + + /** + * GET 请求参数校验失败(格式、数据类型错误) + */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(BindException.class) + public R bindExceptionHandler(BindException e) { + printRequest(); + List allErrors = e.getAllErrors(); + List msg = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()); + log.error("参数校验失败:【{}】", msg, e); + return R.failed(ExceptionMsg.CLI_ERR_MSG, msg); + } + + + /** + * 404 + *

需要配置:

+ *
+     * spring:
+     *   web:
+     *     resources:
+     *       # 关闭静态资源映射
+     *       add-mappings: false
+     *   mvc:
+     *     # 为找到的资源抛出异常
+     *     throw-exception-if-no-handler-found: true
+     */
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(NoHandlerFoundException.class)
+    public R noHandlerFoundExceptionHandler(NoHandlerFoundException e) {
+        printRequest();
+        String httpMethod = e.getHttpMethod();
+        String requestURL = e.getRequestURL();
+        log.error("资源不存在:[【{}】【{}】]", httpMethod, requestURL, e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, "资源不存在,【{}】【{}】", httpMethod, requestURL);
+    }
+
+    /**
+     * 无法生成需要的响应结果
+     */
+    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
+    @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
+    public R httpMediaTypeNotAcceptableExceptionHandler(HttpMediaTypeNotAcceptableException e) {
+        printRequest();
+        log.error("无法生成需要的响应结果", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "无法生成需要的响应结果,{}", e.getMessage());
+    }
+
+    /**
+     * 路径参数名称不匹配
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(MissingPathVariableException.class)
+    public R missingPathVariableExceptionHandler(MissingPathVariableException e) {
+        printRequest();
+        String variableName = e.getVariableName();
+        log.error("路径参数名称与 URL 模板不匹配, 需要的路径参数名称:【{}】, URL 模板中未找到此名称对应的参数", variableName, e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, "路径参数名称与 URL 模板不匹配, 需要的路径参数名称:【{}】, URL 模板中未找到此名称对应的参数", variableName);
+    }
+
+    /**
+     * 没有合适的转换器
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(ConversionNotSupportedException.class)
+    public R conversionNotSupportedExceptionHandler(ConversionNotSupportedException e) {
+        printRequest();
+        log.error("没有合适的转换器", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "没有合适的转换器,【{}】", e.getMessage());
+    }
+
+    /**
+     * 写出响应信息时出现错误
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(HttpMessageNotWritableException.class)
+    public R httpMessageNotWritableExceptionHandler(HttpMessageNotWritableException e) {
+        printRequest();
+        log.error("写出响应信息时出现错误", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "响应时出现错误,【{}】", e.getMessage());
+    }
+
+    /**
+     * 异步请求超时
+     */
+    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+    @ExceptionHandler(AsyncRequestTimeoutException.class)
+    public R asyncRequestTimeoutExceptionHandler(AsyncRequestTimeoutException e) {
+        printRequest();
+        log.error("下游服务响应超时", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "服务器错误, 下游服务响应超时。{}", e.getMessage());
+    }
+
+    /**
+     * 参数错误
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(IllegalArgumentException.class)
+    public R cliExceptionHandler(IllegalArgumentException e) {
+        printRequest();
+        log.error("客户端参数错误", e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, e.getMessage());
+    }
+
+    /**
+     * 客户端错误
+     */
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(CliException.class)
+    public R cliExceptionHandler(CliException e) {
+        printRequest();
+        log.error(e.getMessage(), e);
+        return R.failed(ExceptionMsg.CLI_ERR_MSG, e.message);
+    }
+
+    /**
+     * 系统异常
+     */
+    @ResponseStatus(HttpStatus.OK)
+    @ExceptionHandler(SysException.class)
+    public R sysExceptionHandler(SysException e) {
+        printRequest();
+        log.error(e.getMessage(), e);
+        return R.failed(e.expect, e.msg, e.message);
+    }
+
+    /**
+     * 系统错误
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(SysError.class)
+    public R sysErrorHandler(SysError e) {
+        printRequest();
+        log.error(e.getMessage(), e);
+        return R.failed(e.expect, e.msg, e.message);
+    }
+
+    /**
+     * 服务器错误
+     */
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(Throwable.class)
+    public R exceptionHandler(Throwable e) {
+        printRequest();
+        log.error("内部服务错误", e);
+        return R.failed(ExceptionMsg.SYS_ERR_MSG, "内部服务错误");
+    }
+
+    /**
+     * 打印日志
+     */
+    private void printRequest() {
+        try {
+            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
+            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+            String method = request.getMethod();
+            String requestURI = request.getRequestURI();
+            log.error("接口请求异常:【{}】【{}】", method, requestURI);
+        } catch (Throwable e) {
+            log.error("接口请求信息打印失败", e);
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java
new file mode 100644
index 0000000..001d477
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableHttpServletRequest.java
@@ -0,0 +1,148 @@
+package com.njzscloud.common.mvc.support;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.jackson.Jackson;
+import com.njzscloud.common.mvc.util.ServletUtil;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class ReusableHttpServletRequest extends HttpServletRequestWrapper {
+    private final byte[] body;
+
+    public ReusableHttpServletRequest(HttpServletRequest request) {
+        super(request);
+        try {
+            if (ServletUtil.isMultipartContent(request)) {
+                body = "多部分表单".getBytes(StandardCharsets.UTF_8);
+            } else {
+                ServletInputStream inputStream = request.getInputStream();
+                body = IoUtil.readBytes(inputStream);
+            }
+            printRequest();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void printRequest() {
+        try {
+            String method = this.getMethod();
+            String requestURI = this.getRequestURI();
+            Map parameterMap = this.getParameterMap();
+            Enumeration headerNames = this.getHeaderNames();
+            Map headerMap = new HashMap<>();
+            while (headerNames.hasMoreElements()) {
+                String headerName = headerNames.nextElement();
+                String header = this.getHeader(headerName);
+                headerMap.put(headerName, header);
+            }
+            String body;
+            if (method.equalsIgnoreCase("GET")) {
+                body = "无";
+            } else {
+                if (this.body == null || this.body.length == 0) {
+                    body = "无";
+                } else {
+                    body = new String(this.body, StandardCharsets.UTF_8);
+                    body = StrUtil.isBlank(body) ? "无" : body;
+                }
+            }
+            if (requestURI.contains("/station/letter")) {
+                return;
+            }
+            log.info("接口:{} {}\n请求头:{}\n请求参数:{}\n请求体:{}",
+                method, requestURI, headerMap.isEmpty() ? "无" : Jackson.toJsonStr(headerMap), parameterMap.isEmpty() ? "无" : Jackson.toJsonStr(parameterMap), body);
+        } catch (Throwable e) {
+            log.error("接口请求信息打印失败", e);
+        }
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+        return new ServletInputStream() {
+            @Override
+            public int read() throws IOException {
+                return bais.read();
+            }
+
+            @Override
+            public boolean isFinished() {
+                return false;
+            }
+
+            @Override
+            public boolean isReady() {
+                return true;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {
+            }
+
+            @Override
+            public int readLine(byte[] b, int off, int len) throws IOException {
+                return super.readLine(b, off, len);
+            }
+
+            @Override
+            public int read(byte[] b) throws IOException {
+                return bais.read(b);
+            }
+
+            @Override
+            public int read(byte[] b, int off, int len) throws IOException {
+                return bais.read(b, off, len);
+            }
+
+            @Override
+            public long skip(long n) throws IOException {
+                return bais.skip(n);
+            }
+
+            @Override
+            public int available() throws IOException {
+                return bais.available();
+            }
+
+            @Override
+            public void close() throws IOException {
+                bais.close();
+            }
+
+            @Override
+            public synchronized void mark(int readlimit) {
+                bais.mark(readlimit);
+            }
+
+            @Override
+            public synchronized void reset() throws IOException {
+                bais.reset();
+            }
+
+            @Override
+            public boolean markSupported() {
+                return bais.markSupported();
+            }
+        };
+    }
+
+    @Override
+    public BufferedReader getReader() throws IOException {
+        return new BufferedReader(new InputStreamReader(getInputStream()));
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java
new file mode 100644
index 0000000..4fab3d9
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/support/ReusableRequestFilter.java
@@ -0,0 +1,25 @@
+package com.njzscloud.common.mvc.support;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+@Slf4j
+public class ReusableRequestFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+        throws IOException, ServletException {
+        /* HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+        if ("Upgrade".equalsIgnoreCase(httpServletRequest.getHeader("connection"))
+                || "Upgrade".equalsIgnoreCase(httpServletRequest.getHeader("Connection"))
+        ) {
+            chain.doFilter(request, response);
+        } else {
+            chain.doFilter(new ReusableHttpServletRequest((HttpServletRequest) request), response);
+        } */
+        chain.doFilter(new ReusableHttpServletRequest((HttpServletRequest) request), response);
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/ServletUtil.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/ServletUtil.java
new file mode 100644
index 0000000..c8bb865
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/util/ServletUtil.java
@@ -0,0 +1,122 @@
+package com.njzscloud.common.mvc.util;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import com.njzscloud.common.core.ex.Exceptions;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.Locale;
+
+@Slf4j
+public class ServletUtil {
+    public static boolean isMultipartContent(HttpServletRequest request) {
+        if (!"POST".equalsIgnoreCase(request.getMethod())) {
+            return false;
+        }
+        String contentType = request.getContentType();
+        if (contentType == null) {
+            return false;
+        }
+        return contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/");
+    }
+
+    public static void write(HttpServletResponse response, String text, String contentType) {
+        response.setContentType(contentType);
+        Writer writer = null;
+        try {
+            writer = response.getWriter();
+            writer.write(text);
+            writer.flush();
+        } catch (IOException e) {
+            throw Exceptions.error(e, "数据响应失败");
+        } finally {
+            IoUtil.close(writer);
+        }
+    }
+
+    /**
+     * 预览文件
+     *
+     * @param response    HttpServletResponse
+     * @param inputStream 文件流
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void preview(HttpServletResponse response, InputStream inputStream, String contentType, String fileName) {
+        ServletUtil.writeFile(response, inputStream, contentType, "inline" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 预览文件
+     *
+     * @param response    HttpServletResponse
+     * @param bytes       文件数据
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void preview(HttpServletResponse response, byte[] bytes, String contentType, String fileName) {
+        ServletUtil.writeFile(response, bytes, contentType, "inline" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 下载文件
+     *
+     * @param response    HttpServletResponse
+     * @param inputStream 文件流
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void download(HttpServletResponse response, InputStream inputStream, String contentType, String fileName) {
+        ServletUtil.writeFile(response, inputStream, contentType, "attachment" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 下载文件
+     *
+     * @param response    HttpServletResponse
+     * @param bytes       文件数据
+     * @param contentType MIME
+     * @param fileName    文件名
+     */
+    public static void download(HttpServletResponse response, byte[] bytes, String contentType, String fileName) {
+        ServletUtil.writeFile(response, bytes, contentType, "attachment" + (StrUtil.isNotBlank(fileName) ? ";filename=" + URLUtil.encode(fileName) : ""));
+    }
+
+    /**
+     * 向客户端响应文件
+     *
+     * @param response           HttpServletResponse
+     * @param data               文件内容, 只能是 InputStream 或 byte[]
+     * @param contentType        MIME
+     * @param contentDisposition 请求头 Content-Disposition
+     */
+    public static void writeFile(HttpServletResponse response, Object data, String contentType, String contentDisposition) {
+        ServletOutputStream outputStream = null;
+        try {
+            response.setContentType(contentType);
+            response.setCharacterEncoding(CharsetUtil.UTF_8);
+            response.setHeader("Content-Disposition", contentDisposition);
+            outputStream = response.getOutputStream();
+            if (data instanceof InputStream) {
+                IoUtil.copy((InputStream) data, outputStream);
+            } else if (data instanceof byte[]) {
+                IoUtil.write(outputStream, false, (byte[]) data);
+            }
+        } catch (Exception e) {
+            log.error("文件响应失败", e);
+        } finally {
+            IoUtil.close(outputStream);
+            if (data instanceof InputStream) {
+                IoUtil.close((InputStream) data);
+            }
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java
new file mode 100644
index 0000000..1574587
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constrained.java
@@ -0,0 +1,5 @@
+package com.njzscloud.common.mvc.validator;
+
+public interface Constrained {
+    ValidRule[] rules();
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java
new file mode 100644
index 0000000..d8a422d
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/Constraint.java
@@ -0,0 +1,18 @@
+package com.njzscloud.common.mvc.validator;
+
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@jakarta.validation.Constraint(validatedBy = ConstraintValidator.class)
+@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
+public @interface Constraint {
+    String message() default "";
+
+    Class[] groups() default {};
+
+    Class[] payload() default {};
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java
new file mode 100644
index 0000000..cadbb1d
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ConstraintValidator.java
@@ -0,0 +1,31 @@
+package com.njzscloud.common.mvc.validator;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ConstraintValidator implements jakarta.validation.ConstraintValidator {
+
+    @Override
+    public boolean isValid(Constrained valid, ConstraintValidatorContext ctx) {
+        ValidRule[] rules = valid.rules();
+
+        if (rules == null || rules.length == 0) return true;
+
+        List messageList = Arrays.stream(rules)
+            .filter(it -> !it.predicate.get())
+            .map(it -> it.message)
+            .collect(Collectors.toList());
+
+        if (messageList.isEmpty()) return true;
+
+        ctx.disableDefaultConstraintViolation();
+        ctx.buildConstraintViolationWithTemplate(StrUtil.join(";\n", messageList))
+            .addConstraintViolation();
+
+        return false;
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java
new file mode 100644
index 0000000..5fe392e
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/java/com/njzscloud/common/mvc/validator/ValidRule.java
@@ -0,0 +1,22 @@
+package com.njzscloud.common.mvc.validator;
+
+
+import java.util.function.Supplier;
+
+/**
+ * 校验规则
+ */
+public class ValidRule {
+    public final Supplier predicate;
+    public final String message;
+
+    private ValidRule(Supplier predicate, String message) {
+        this.predicate = predicate;
+        this.message = message;
+    }
+
+    public static ValidRule of(Supplier predicate, String message) {
+        return new ValidRule(predicate, message);
+    }
+}
+
diff --git a/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..314aeeb
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-mvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+com.njzscloud.common.mvc.config.MvcAutoConfiguration
+com.njzscloud.common.mvc.config.RequestMappingHandlerAdapterAutoConfiguration
diff --git a/njzscloud-common/njzscloud-common-oss/pom.xml b/njzscloud-common/njzscloud-common-oss/pom.xml
new file mode 100644
index 0000000..24f272b
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/pom.xml
@@ -0,0 +1,43 @@
+
+
+    4.0.0
+    
+        com.njzscloud
+        njzscloud-common
+        0.0.1
+    
+
+    njzscloud-common-oss
+    jar
+
+    
+        21
+        21
+        UTF-8
+    
+
+    
+        
+            com.njzscloud
+            njzscloud-common-core
+            provided
+        
+        
+            com.njzscloud
+            njzscloud-common-mvc
+            provided
+        
+        
+            io.minio
+            minio
+        
+        
+            com.aliyun.oss
+            aliyun-sdk-oss
+            3.17.4
+        
+    
+
+
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSAutoConfiguration.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSAutoConfiguration.java
new file mode 100644
index 0000000..4b5b4d7
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSAutoConfiguration.java
@@ -0,0 +1,75 @@
+package com.njzscloud.common.oss.config;
+
+
+import com.aliyun.oss.ClientBuilderConfiguration;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.common.auth.DefaultCredentialProvider;
+import com.aliyun.oss.common.comm.SignVersion;
+import com.njzscloud.common.oss.controller.OSSController;
+import com.njzscloud.common.oss.service.AliService;
+import com.njzscloud.common.oss.service.MinioService;
+import com.njzscloud.common.oss.service.OSSService;
+import io.minio.MinioAsyncClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnBooleanProperty(prefix = "oss", name = "enable", matchIfMissing = true)
+@EnableConfigurationProperties(OSSProperties.class)
+public class OSSAutoConfiguration {
+
+    @Bean(destroyMethod = "close")
+    @ConditionalOnProperty(prefix = "oss", name = "type", havingValue = "minio")
+    public MinioAsyncClient minioClient(OSSProperties ossProperties) {
+        OSSProperties.MinioProperties minio = ossProperties.getMinio();
+        String endpoint = minio.getEndpoint();
+        String accessKey = minio.getAccessKey();
+        String secretKey = minio.getSecretKey();
+        return MinioAsyncClient.builder()
+            .endpoint(endpoint)
+            .credentials(accessKey, secretKey)
+            .build();
+    }
+
+    @Bean(destroyMethod = "shutdown")
+    @ConditionalOnProperty(prefix = "oss", name = "type", havingValue = "ali")
+    public OSS ossClient(OSSProperties ossProperties) {
+        OSSProperties.AliProperties ali = ossProperties.getAli();
+        String endpoint = ali.getEndpoint();
+        String accessKey = ali.getAccessKey();
+        String secretKey = ali.getSecretKey();
+        String region = ali.getRegion();
+
+        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
+        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
+        DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKey, secretKey);
+        return OSSClientBuilder.create()
+            .endpoint(endpoint)
+            .region(region)
+            .credentialsProvider(credentialProvider)
+            .clientConfiguration(clientBuilderConfiguration)
+            .build();
+    }
+
+    @Bean
+    public OSSService ossService(OSSProperties ossProperties) {
+        OSSType type = ossProperties.getType();
+        switch (type) {
+            case ali:
+                return new AliService();
+            case minio:
+                return new MinioService();
+            default:
+                return null;
+        }
+    }
+
+    @Bean
+    public OSSController ossController(OSSService ossService) {
+        return new OSSController(ossService);
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSProperties.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSProperties.java
new file mode 100644
index 0000000..208f1fd
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSProperties.java
@@ -0,0 +1,34 @@
+package com.njzscloud.common.oss.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Getter
+@Setter
+@ConfigurationProperties("oss")
+public class OSSProperties {
+    private boolean enable = true;
+    private OSSType type;
+    private MinioProperties minio;
+    private AliProperties ali;
+
+    @Getter
+    @Setter
+    public static class MinioProperties {
+        private String endpoint;
+        private String accessKey;
+        private String secretKey;
+        private String bucketName;
+    }
+
+    @Getter
+    @Setter
+    public static class AliProperties {
+        private String region;
+        private String endpoint;
+        private String accessKey;
+        private String secretKey;
+        private String bucketName;
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSType.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSType.java
new file mode 100644
index 0000000..f16e2fd
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/config/OSSType.java
@@ -0,0 +1,6 @@
+package com.njzscloud.common.oss.config;
+
+public enum OSSType {
+    minio,
+    ali
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/controller/OSSController.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/controller/OSSController.java
new file mode 100644
index 0000000..1464f51
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/controller/OSSController.java
@@ -0,0 +1,60 @@
+package com.njzscloud.common.oss.controller;
+
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.core.utils.R;
+import com.njzscloud.common.oss.service.OSSService;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/oss")
+@RequiredArgsConstructor
+public class OSSController {
+    private final OSSService ossService;
+
+    @GetMapping("/start_mlt_upload")
+    public R>> startMltUpload(
+        String objectName,
+        String contentType,
+        int partNum) {
+        return R.success(ossService.startMltUpload(objectName, contentType, partNum));
+    }
+
+    @GetMapping("/end_mlt_upload")
+    public R endMltUpload(String objectName, String uploadId, int partNum) {
+        ossService.endMltUpload(objectName, uploadId, partNum);
+        return R.success();
+    }
+
+    /**
+     * 获取上传链接
+     */
+    @GetMapping("/obtain_presigned_url")
+    public R> obtainPresignedUrl(@RequestParam(value = "filename") String filename) {
+        return R.success(ossService.obtainPresignedUrl(filename));
+    }
+
+    /**
+     * 上传文件
+     */
+    @PostMapping("/upload")
+    public R uploadFile(@RequestPart("file") MultipartFile file) {
+        return R.success(ossService.uploadFile(file));
+    }
+
+    /**
+     * 下载文件
+     */
+    @GetMapping("/download/{bucketName}/{objectName}")
+    public void obtainFile(
+        @PathVariable("bucketName") String bucketName,
+        @PathVariable("objectName") String objectName,
+        HttpServletResponse response) {
+        ossService.obtainFile(bucketName, objectName, response);
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/AliService.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/AliService.java
new file mode 100644
index 0000000..22a923e
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/AliService.java
@@ -0,0 +1,62 @@
+package com.njzscloud.common.oss.service;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.ex.Exceptions;
+import com.njzscloud.common.core.ex.SysThrowable;
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.mvc.util.ServletUtil;
+import com.njzscloud.common.oss.util.AliOSS;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class AliService implements OSSService {
+    @Override
+    public Tuple2> startMltUpload(String objectName, String contentType, int partNum) {
+        throw Exceptions.error("暂未实现");
+    }
+
+    @Override
+    public void endMltUpload(String objectName, String uploadId, int partNum) {
+        throw Exceptions.error("暂未实现");
+    }
+
+    @Override
+    public Map obtainPresignedUrl(String filename) {
+        String objectName = IdUtil.fastSimpleUUID();
+        if (StrUtil.isNotBlank(filename)) {
+            objectName = objectName + "." + FileUtil.extName(filename);
+        }
+        return AliOSS.obtainPresignedUrl(objectName);
+    }
+
+    @Override
+    public void obtainFile(String bucketName, String objectName, HttpServletResponse response) {
+        Tuple2 tuple2 = AliOSS.obtainFile(bucketName, objectName);
+        ServletUtil.download(response, tuple2.get_0(), tuple2.get_1(), objectName);
+    }
+
+    @Override
+    public String uploadFile(MultipartFile file) {
+        String contentType = file.getContentType();
+        String originalFilename = file.getOriginalFilename();
+        String objectName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(originalFilename);
+
+        try {
+            InputStream inputStream = file.getInputStream();
+            return AliOSS.uploadFile(objectName, contentType, inputStream);
+        } catch (SysThrowable e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("上传文件失败", e);
+            throw Exceptions.error("上传文件失败");
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/MinioService.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/MinioService.java
new file mode 100644
index 0000000..b14b05c
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/MinioService.java
@@ -0,0 +1,62 @@
+package com.njzscloud.common.oss.service;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.ex.Exceptions;
+import com.njzscloud.common.core.ex.SysThrowable;
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.mvc.util.ServletUtil;
+import com.njzscloud.common.oss.util.Minio;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class MinioService implements OSSService {
+    @Override
+    public Tuple2> startMltUpload(String objectName, String contentType, int partNum) {
+        return Minio.startMltUpload(objectName, contentType, partNum);
+    }
+
+    @Override
+    public void endMltUpload(String objectName, String uploadId, int partNum) {
+        Minio.endMltUpload(objectName, uploadId, partNum);
+    }
+
+    @Override
+    public Map obtainPresignedUrl(String filename) {
+        String objectName = IdUtil.fastSimpleUUID();
+        if (StrUtil.isNotBlank(filename)) {
+            objectName = objectName + "." + FileUtil.extName(filename);
+        }
+        return Minio.obtainPresignedUrl(objectName);
+    }
+
+    @Override
+    public void obtainFile(String bucketName, String objectName, HttpServletResponse response) {
+        Tuple2 tuple2 = Minio.obtainFile(bucketName, objectName);
+        ServletUtil.download(response, tuple2.get_0(), tuple2.get_1(), objectName);
+    }
+
+    @Override
+    public String uploadFile(MultipartFile file) {
+        String contentType = file.getContentType();
+        String originalFilename = file.getOriginalFilename();
+        String objectName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(originalFilename);
+
+        try {
+            InputStream inputStream = file.getInputStream();
+            return Minio.uploadFile(objectName, contentType, inputStream);
+        } catch (SysThrowable e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("上传文件失败", e);
+            throw Exceptions.error("上传文件失败");
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/OSSService.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/OSSService.java
new file mode 100644
index 0000000..bc46542
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/service/OSSService.java
@@ -0,0 +1,32 @@
+package com.njzscloud.common.oss.service;
+
+import com.njzscloud.common.core.tuple.Tuple2;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.Map;
+
+public interface OSSService {
+    /**
+     * 开始分片上传
+     */
+    Tuple2> startMltUpload(String objectName, String contentType, int partNum);
+
+    /**
+     * 结束分片上传
+     */
+    void endMltUpload(String objectName, String uploadId, int partNum);
+
+    /**
+     * 获取上传链接
+     */
+    Map obtainPresignedUrl(String filename);
+
+    /**
+     * 获取文件
+     */
+    void obtainFile(String bucketName, String objectName, HttpServletResponse response);
+
+    String uploadFile(MultipartFile file);
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/AliOSS.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/AliOSS.java
new file mode 100644
index 0000000..438d028
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/AliOSS.java
@@ -0,0 +1,143 @@
+package com.njzscloud.common.oss.util;
+
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import com.aliyun.oss.ClientException;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSException;
+import com.aliyun.oss.common.utils.BinaryUtil;
+import com.aliyun.oss.model.GetObjectRequest;
+import com.aliyun.oss.model.OSSObject;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.aliyun.oss.model.PolicyConditions;
+import com.njzscloud.common.core.ex.Exceptions;
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.oss.config.OSSProperties;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AliOSS {
+    private static final OSS CLIENT;
+    private static final String BUCKET_NAME;
+    private static final String ACCESS_KEY;
+
+    static {
+        CLIENT = SpringUtil.getBean(OSS.class);
+        OSSProperties ossProperties = SpringUtil.getBean(OSSProperties.class);
+        OSSProperties.AliProperties ali = ossProperties.getAli();
+        BUCKET_NAME = ali.getBucketName();
+        ACCESS_KEY = ali.getAccessKey();
+        if (StrUtil.isNotBlank(BUCKET_NAME)) {
+            createBucket(BUCKET_NAME);
+        }
+    }
+
+    public static void createBucket(String bucketName) {
+        Assert.notBlank(bucketName, "未指明存储位置");
+        try {
+            boolean exists = CLIENT.doesBucketExist(bucketName);
+            if (exists) return;
+            CLIENT.createBucket(bucketName);
+        } catch (Exception e) {
+            log.error("存储桶创建失败", e);
+        }
+    }
+
+    public static Map obtainPresignedUrl(String objectName) {
+        try {
+            long expireEndTime = System.currentTimeMillis() + 3600 * 1000;
+            Date expiration = new Date(expireEndTime);
+            PolicyConditions policyConds = new PolicyConditions();
+            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
+            String postPolicy = CLIENT.generatePostPolicy(expiration, policyConds);
+            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
+            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
+            String postSignature = CLIENT.calculatePostSignature(postPolicy);
+            Map data = new HashMap<>();
+            data.put("OSSAccessKeyId", ACCESS_KEY);
+            data.put("policy", encodedPolicy);
+            data.put("signature", postSignature);
+            data.put("success_action_status", "200");
+            data.put("name", objectName);
+            data.put("key", objectName);
+            data.put("bucketName", BUCKET_NAME);
+            data.put("objectName", objectName);
+            return data;
+        } catch (OSSException oe) {
+            log.error("阿里云获取上传链接失败,错误信息:{}、错误码:{}、请求标识:{}、主机标识:{}、存储桶:{}、对象名称:{}",
+                oe.getErrorMessage(),
+                oe.getErrorCode(),
+                oe.getRequestId(),
+                oe.getHostId(),
+                BUCKET_NAME,
+                objectName);
+            throw Exceptions.error("文件服务器错误");
+        } catch (ClientException ce) {
+            log.error("阿里云获取上传链接失败,错误信息:{}、存储桶:{}、对象名称:{}",
+                ce.getMessage(),
+                BUCKET_NAME,
+                objectName);
+            throw Exceptions.error("文件服务器错误");
+        }
+    }
+
+    public static Tuple2 obtainFile(String bucketName, String objectName) {
+        try {
+            OSSObject response = CLIENT.getObject(new GetObjectRequest(bucketName, objectName));
+            ObjectMetadata objectMetadata = response.getObjectMetadata();
+            String contentType = objectMetadata.getContentType();
+            InputStream objectContent = response.getObjectContent();
+            return Tuple2.of(objectContent, contentType);
+        } catch (OSSException oe) {
+            log.error("阿里云下载文件失败,错误信息:{}、错误码:{}、请求标识:{}、主机标识:{}、存储桶:{}、对象名称:{}",
+                oe.getErrorMessage(),
+                oe.getErrorCode(),
+                oe.getRequestId(),
+                oe.getHostId(),
+                bucketName,
+                objectName);
+        } catch (ClientException ce) {
+            log.error("阿里云下载文件失败,错误信息:{}、存储桶:{}、对象名称:{}",
+                ce.getMessage(),
+                bucketName,
+                objectName);
+        }
+        return Tuple2.of(new ByteArrayInputStream(new byte[0]), "");
+    }
+
+    public static String uploadFile(String objectName, String contentType, InputStream inputStream) {
+        try {
+            ObjectMetadata objectMetadata = new ObjectMetadata();
+            if (StrUtil.isNotBlank(contentType)) objectMetadata.setContentType(contentType);
+            CLIENT.putObject(BUCKET_NAME, objectName, inputStream, objectMetadata);
+            return "/" + BUCKET_NAME + "/" + objectName;
+        } catch (OSSException oe) {
+            log.error("阿里云上传文件失败,错误信息:{}、错误码:{}、请求标识:{}、主机标识:{}、存储桶:{}、对象名称:{}",
+                oe.getErrorMessage(),
+                oe.getErrorCode(),
+                oe.getRequestId(),
+                oe.getHostId(),
+                BUCKET_NAME,
+                objectName);
+            throw Exceptions.error("文件服务器错误");
+        } catch (ClientException ce) {
+            log.error("阿里云上传文件失败,错误信息:{}、存储桶:{}、对象名称:{}",
+                ce.getMessage(),
+                BUCKET_NAME,
+                objectName);
+            throw Exceptions.error("文件服务器错误");
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/Minio.java b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/Minio.java
new file mode 100644
index 0000000..562c433
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/java/com/njzscloud/common/oss/util/Minio.java
@@ -0,0 +1,174 @@
+package com.njzscloud.common.oss.util;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import com.google.common.collect.HashMultimap;
+import com.njzscloud.common.core.ex.Exceptions;
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.oss.config.OSSProperties;
+import io.minio.*;
+import io.minio.errors.*;
+import io.minio.http.Method;
+import io.minio.messages.ListPartsResult;
+import io.minio.messages.Part;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class Minio {
+    private static final MinioAsyncClient CLIENT;
+    private static final Map, String> ERROR_MAP = new HashMap<>();
+    private static final String BUCKET_NAME;
+
+    static {
+        ERROR_MAP.put(ErrorResponseException.class, "文件存储服务响应失败");
+        ERROR_MAP.put(InsufficientDataException.class, "文件读取失败");
+        ERROR_MAP.put(InternalException.class, "文件服务器错误");
+        ERROR_MAP.put(InvalidKeyException.class, "缺少 HMAC SHA-256 依赖");
+        ERROR_MAP.put(InvalidResponseException.class, "服务器未响应");
+        ERROR_MAP.put(IOException.class, "文件读取失败");
+        ERROR_MAP.put(NoSuchAlgorithmException.class, "缺少 MD5 或 SHA-256 依赖");
+        ERROR_MAP.put(XmlParserException.class, "数据解析失败");
+        CLIENT = SpringUtil.getBean(MinioAsyncClient.class);
+        BUCKET_NAME = SpringUtil.getBean(OSSProperties.class).getMinio().getBucketName();
+        if (StrUtil.isNotBlank(BUCKET_NAME)) {
+            createBucket(BUCKET_NAME);
+        }
+    }
+
+    public static void createBucket(String bucketName) {
+        Assert.notBlank(bucketName, "未指明存储位置");
+        try {
+            boolean exists = Minio.execSync(() -> CLIENT.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()));
+            if (exists) return;
+            Minio.execSync(() -> CLIENT.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()));
+        } catch (Exception e) {
+            log.error("存储桶创建失败", e);
+        }
+    }
+
+    public static Tuple2> startMltUpload(String objectName, String contentType, int partNum) {
+        HashMultimap header = HashMultimap.create();
+        header.put("Content-Type", contentType);
+        return Minio.execSync(() -> CLIENT
+            .createMultipartUploadAsync(BUCKET_NAME, null, objectName, header, null)
+            .thenApply(it -> {
+                String uploadId = it.result().uploadId();
+                LinkedList urls = new LinkedList<>();
+                for (int i = 0; i < partNum; i++) {
+                    int partNumber = i + 1;
+                    String url = obtainPresignedUrl(objectName + "." + partNumber, MapUtil.builder()
+                        .put("partNumber", Integer.toString(partNumber))
+                        .put("uploadId", uploadId)
+                        .build());
+                    urls.add(url);
+                }
+                return Tuple2.of(uploadId, urls);
+            }));
+    }
+
+    public static void endMltUpload(String objectName, String uploadId, int partNum) {
+        CompletableFuture.runAsync(() -> {
+            for (int i = 0; i < partNum % 1000 + 1; i++) {
+                int partNumberMarker = i + 1;
+                execSync(() -> CLIENT.listPartsAsync(BUCKET_NAME,
+                        null,
+                        objectName,
+                        null,
+                        partNumberMarker,
+                        uploadId,
+                        null, null)
+                    .thenAccept(it -> {
+                        ListPartsResult result = it.result();
+                        List parts = result.partList();
+                        execSync(() -> CLIENT.completeMultipartUploadAsync(BUCKET_NAME,
+                            null,
+                            objectName,
+                            uploadId,
+                            parts.toArray(new Part[0]),
+                            null,
+                            null
+                        ));
+                    })
+                );
+            }
+        });
+    }
+
+    public static String obtainPresignedUrl(String objectName, Map extraQueryParams) {
+        return exec(() -> CLIENT.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
+            .method(Method.PUT)
+            .bucket(BUCKET_NAME)
+            .object(objectName)
+            .expiry(1, TimeUnit.DAYS)
+            .extraQueryParams(extraQueryParams)
+            .build()));
+    }
+
+    public static Map obtainPresignedUrl(String objectName) {
+        return exec(() -> {
+            PostPolicy policy = new PostPolicy(BUCKET_NAME, ZonedDateTime.now().plusDays(1));
+            policy.addEqualsCondition("key", objectName);
+            Map data = CLIENT.getPresignedPostFormData(policy);
+            data.put("key", objectName);
+            data.put("bucketName", BUCKET_NAME);
+            data.put("objectName", objectName);
+            return data;
+        });
+    }
+
+    public static Tuple2 obtainFile(String bucketName, String objectName) {
+        GetObjectResponse response = execSync(() -> CLIENT.getObject(GetObjectArgs.builder()
+            .object(objectName)
+            .bucket(bucketName).build()));
+        return Tuple2.of(response, response.headers().get("Content-Type"));
+    }
+
+    private static  T execSync(MinioExec> fn) {
+        try {
+            return fn.exec().get();
+        } catch (Exception e) {
+            log.error("执行失败,文件服务器错误", e);
+            throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误"));
+        }
+    }
+
+    private static  T exec(MinioExec fn) {
+        try {
+            return fn.exec();
+        } catch (Exception e) {
+            log.error("执行失败,文件服务器错误", e);
+            throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误"));
+        }
+    }
+
+    public static String uploadFile(String objectName, String contentType, InputStream inputStream) {
+        Minio.exec(() -> CLIENT.putObject(PutObjectArgs.builder()
+            .bucket(BUCKET_NAME)
+            .object(objectName)
+            .contentType(contentType)
+            .stream(inputStream, -1, 1024 * 1024)
+            .build()));
+        return "/" + BUCKET_NAME + "/" + objectName;
+    }
+
+    public interface MinioExec {
+        T exec() throws Exception;
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..d763036
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.njzscloud.common.oss.config.OSSAutoConfiguration
diff --git a/njzscloud-common/njzscloud-common-redis/pom.xml b/njzscloud-common/njzscloud-common-redis/pom.xml
new file mode 100644
index 0000000..327afec
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-redis/pom.xml
@@ -0,0 +1,40 @@
+
+
+    4.0.0
+    
+        com.njzscloud
+        njzscloud-common
+        0.0.1
+    
+
+    njzscloud-common-redis
+
+    
+        21
+        21
+        UTF-8
+    
+
+    
+        
+            com.njzscloud
+            njzscloud-common-core
+            provided
+        
+
+        
+        
+            io.lettuce
+            lettuce-core
+        
+
+        
+        
+            org.apache.commons
+            commons-pool2
+        
+
+    
+
diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java
new file mode 100644
index 0000000..bfb5fe0
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/RedisCli.java
@@ -0,0 +1,232 @@
+package com.njzscloud.common.redis;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.njzscloud.common.core.ex.Exceptions;
+import com.njzscloud.common.core.fastjson.Fastjson;
+import com.njzscloud.common.redis.support.RedisFastjsonCodec;
+import com.njzscloud.common.redis.support.RedisMessageDispatch;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.TransactionResult;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.core.codec.RedisCodec;
+import io.lettuce.core.pubsub.RedisPubSubListener;
+import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
+import io.lettuce.core.support.ConnectionPoolSupport;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Redis 客户端,对 lettuce 客户端的封装
+ */
+@Slf4j
+@SuppressWarnings("unchecked")
+public class RedisCli {
+
+    private final RedisCodec codec = new RedisFastjsonCodec();
+
+    /**
+     * lettuce 客户端
+     */
+    @Getter
+    private final RedisClient redisClient;
+
+    /**
+     * 普通连接池
+     */
+    private final GenericObjectPool> pool;
+
+    /**
+     * 发布订阅模式连接池
+     */
+    private final GenericObjectPool> pubSubPool;
+
+    /**
+     * 已订阅的频道模式
+     */
+    private final Set subscribedPatterns = new HashSet<>();
+    /**
+     * 已订阅的频道
+     */
+    private final Set subscribedChannels = new HashSet<>();
+
+    /**
+     * 创建 Redis 客户端
+     *
+     * @param uri        Redis 连接 URI
+     * @param poolConfig 连接池配置
+     * @param pubsub     是否开启发布订阅模式支持
+     */
+    public RedisCli(RedisURI uri, GenericObjectPoolConfig poolConfig, boolean pubsub) {
+        redisClient = RedisClient.create(uri);
+
+        pool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(codec), (GenericObjectPoolConfig>) poolConfig);
+        if (pubsub) pubSubPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connectPubSub(codec), (GenericObjectPoolConfig>) poolConfig);
+        else pubSubPool = null;
+    }
+
+    /**
+     * 退出
+     */
+    public void exit() {
+        if (pubSubPool != null) {
+            punsubscribe(subscribedPatterns);
+            unsubscribe(subscribedChannels);
+            pubSubPool.close();
+        }
+        pool.close();
+        redisClient.shutdown();
+    }
+
+    /**
+     * 执行 Redis 命令
+     *
+     * @param fn  要执行的操作
+     * @param  Redis 值类型
+     * @param  返回值类型
+     * @return 返回 fn 的返回值
+     */
+    public  R exec(Function, R> fn) {
+        try (StatefulRedisConnection connection = pool.borrowObject()) {
+            return fn.apply((RedisCommands) connection.sync());
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 操作执行失败");
+        }
+    }
+
+    /**
+     * 执行 Redis 命令(带事务)
+     *
+     * @param fn  要执行的操作
+     * @param  Redis 值类型
+     * @return {@link TransactionResult} 事务结果
+     */
+    public  TransactionResult execWithTransaction(Consumer> fn) {
+        try (StatefulRedisConnection connection = pool.borrowObject()) {
+            RedisCommands commands = (RedisCommands) connection.sync();
+            commands.multi();
+            fn.accept(commands);
+            return commands.exec();
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 操作执行失败");
+        }
+    }
+
+    /**
+     * 添加监听器
+     *
+     * @see RedisMessageDispatch
+     */
+    public void addListener(RedisPubSubListener redisPubSubListener) {
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            connection.addListener(redisPubSubListener);
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 监听器注册失败");
+        }
+    }
+
+    /**
+     * 发布消息
+     *
+     * @param channel 频道
+     * @param message 消息内容
+     */
+    public void publish(String channel, Object message) {
+        if (log.isDebugEnabled()) {
+            log.debug("发布消息,频道:【{}】,消息:【{}】", channel, Fastjson.toJsonStr(message));
+        }
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            connection.sync().publish(channel, message);
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 消息发布失败,频道:【{}】,消息:【{}】", channel, Fastjson.toJsonStr(message));
+        }
+    }
+
+    /**
+     * 订阅
+     *
+     * @param channels 频道
+     */
+    public void subscribe(Collection channels) {
+        if (log.isDebugEnabled()) {
+            log.debug("订阅,频道:【{}】", channels);
+        }
+        Collection newChannels = CollUtil.subtract(CollUtil.distinct(channels), subscribedChannels);
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            if (newChannels.isEmpty()) return;
+            connection.sync().subscribe(ArrayUtil.toArray(newChannels, String.class));
+            subscribedChannels.addAll(newChannels);
+            if (log.isDebugEnabled()) {
+                log.debug("当前已订阅的频道:【{}】", subscribedChannels);
+            }
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 订阅失败,频道:【{}】", channels);
+        }
+    }
+
+    /**
+     * 取消订阅
+     *
+     * @param channels 频道
+     */
+    public void unsubscribe(Collection channels) {
+        if (log.isDebugEnabled()) {
+            log.debug("订阅,频道:【{}】", channels);
+        }
+        if (channels == null || channels.isEmpty()) return;
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            connection.sync().unsubscribe(ArrayUtil.toArray(channels, String.class));
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 取消订阅失败,频道:【{}】", channels);
+        }
+    }
+
+    /**
+     * 订阅(使用匹配模式)
+     *
+     * @param patterns 频道匹配模式(支持 glob 匹配规则)
+     */
+    public void psubscribe(Collection patterns) {
+        if (log.isDebugEnabled()) {
+            log.debug("订阅(使用匹配模式),频道:【{}】", patterns);
+        }
+        Collection newChannels = CollUtil.subtract(CollUtil.distinct(patterns), subscribedPatterns);
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            if (newChannels.isEmpty()) return;
+            connection.sync().psubscribe(ArrayUtil.toArray(newChannels, String.class));
+            subscribedPatterns.addAll(newChannels);
+            if (log.isDebugEnabled()) {
+                log.debug("当前已订阅的频道(使用匹配模式):【{}】", subscribedPatterns);
+            }
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 订阅失败(使用匹配模式),频道:【{}】", patterns);
+        }
+    }
+
+    /**
+     * 取消订阅(使用匹配模式)
+     *
+     * @param patterns 频道匹配模式(支持 glob 匹配规则)
+     */
+    public void punsubscribe(Collection patterns) {
+        if (log.isDebugEnabled()) {
+            log.debug("取消订阅(使用匹配模式),频道:【{}】", patterns);
+        }
+        if (patterns == null || patterns.isEmpty()) return;
+        try (StatefulRedisPubSubConnection connection = pubSubPool.borrowObject()) {
+            connection.sync().punsubscribe(ArrayUtil.toArray(patterns, String.class));
+        } catch (Exception e) {
+            throw Exceptions.error(e, "Redis 取消订阅失败,(使用匹配模式),频道:【{}】", patterns);
+        }
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java
new file mode 100644
index 0000000..b494b10
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/Eg.java
@@ -0,0 +1,38 @@
+package com.njzscloud.common.redis.annotation;
+
+/**
+ * Redis 监听器使用示例,
+ * 在类上使用 @RedisListener 在方法上使用 @RedisChannel
+ */
+// @RedisListener
+public class Eg {
+    /**
+     * 无参
+     * 注解 @RedisChannel 的参数二选一
+     */
+    @RedisChannel(patterns = {"a*"}, channels = {"aa"})
+    public void a1() {
+    }
+
+    /**
+     * 一个参数
+     * 注解 @RedisChannel 的参数二选一
+     *
+     * @param message 消息内容(类型不限)
+     */
+    @RedisChannel(patterns = {"a*"}, channels = {"aa"})
+    public void a2(Object message) {
+    }
+
+    /**
+     * 三个参数
+     * 注解 @RedisChannel 的参数二选一
+     *
+     * @param pattern 频道匹配模式
+     * @param channel 频道名称
+     * @param message 消息内容 (类型不限)
+     */
+    @RedisChannel(patterns = {"a*"}, channels = {"aa"})
+    public void a3(String pattern, String channel, Object message) {
+    }
+}
diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java
new file mode 100644
index 0000000..0040de9
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisChannel.java
@@ -0,0 +1,33 @@
+package com.njzscloud.common.redis.annotation;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Redis 订阅通道
+ * 

使用方式 {Eg}

+ */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedisChannel { + /** + * 匹配模式 + * 支持的 glob 样式模式: + * h?llo订阅 和hellohallohxllo + * h*llo订阅和hlloheeeello + * h[ae]llo订阅 和 但不订阅hellohallo,hillo + * + * @return String[] 匹配模式 + */ + String[] patterns() default {}; + + /** + * 频道名称 + * + * @return String[] 频道名称 + */ + String[] channels() default {}; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java new file mode 100644 index 0000000..252f71a --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/annotation/RedisListener.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.redis.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * Redis 监听器 + *

使用方式 {Eg}

+ */ +@Inherited +@Component +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedisListener { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java new file mode 100644 index 0000000..6e00853 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisOtherProperties.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.redis.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Redis 配置信息 + */ +@Getter +@Setter +@ConfigurationProperties("spring.redis") +public class RedisOtherProperties { + /** + * 是否启用 Redis + */ + private boolean enable = false; + /** + * 是否使用发布订阅功能 + */ + private boolean pubsub = false; +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java new file mode 100644 index 0000000..4cc134e --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/config/RedisServiceAutoConfiguration.java @@ -0,0 +1,122 @@ +package com.njzscloud.common.redis.config; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.redis.RedisCli; +import com.njzscloud.common.redis.annotation.RedisChannel; +import com.njzscloud.common.redis.annotation.RedisListener; +import com.njzscloud.common.redis.support.RedisMessageDispatch; +import io.lettuce.core.RedisCredentials; +import io.lettuce.core.RedisURI; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Map; + +/** + * Redis 配置类 + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.redis.enable", havingValue = "true") +@EnableConfigurationProperties({RedisProperties.class, RedisOtherProperties.class}) +public class RedisServiceAutoConfiguration { + + /** + * Redis 客户端配置 + * + * @param redisProperties 配置(Spring 自带的配置) + * @param redisOtherProperties 配置(自定义配置) + * @return {@link RedisCli} + */ + @Bean(destroyMethod = "exit") + public RedisCli redisCli(RedisProperties redisProperties, RedisOtherProperties redisOtherProperties) { + String url = redisProperties.getUrl(); + RedisURI redisURI; + if (url == null || url.isEmpty()) { + redisURI = new RedisURI(); + redisURI.setSsl(false); + String username = redisProperties.getUsername(); + String password = redisProperties.getPassword(); + + redisURI.setCredentialsProvider(() -> Mono.just(RedisCredentials.just(username, password))); + + redisURI.setHost(redisProperties.getHost()); + redisURI.setPort(redisProperties.getPort()); + redisURI.setDatabase(redisProperties.getDatabase()); + Duration timeout = redisProperties.getTimeout(); + if (timeout != null) redisURI.setTimeout(timeout); + String clientName = redisProperties.getClientName(); + if (StrUtil.isNotBlank(clientName)) redisURI.setClientName(clientName); + } else { + redisURI = RedisURI.create(url); + } + + RedisProperties.Lettuce lettuce = redisProperties.getLettuce(); + RedisProperties.Pool lettucePool = lettuce.getPool(); + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxTotal(lettucePool.getMaxActive()); + poolConfig.setMaxIdle(lettucePool.getMaxIdle()); + poolConfig.setMinIdle(lettucePool.getMinIdle()); + Duration timeBetweenEvictionRuns = lettucePool.getTimeBetweenEvictionRuns(); + if (timeBetweenEvictionRuns != null) poolConfig.setTimeBetweenEvictionRuns(timeBetweenEvictionRuns); + + Duration maxWait = lettucePool.getMaxWait(); + if (maxWait != null) poolConfig.setMaxWait(maxWait); + + + return new RedisCli(redisURI, poolConfig, redisOtherProperties.isPubsub()); + } + + + /** + * 发布订阅模式的配置 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "spring.redis.pubsub", havingValue = "true") + public static class RedisPubSubConfiguration { + + /** + * 配置消息派发器 + * + * @param redisCli Redis 客户端 {@link RedisCli} + * @return {@link RedisMessageDispatch} + */ + @Bean + public RedisMessageDispatch redisMessageDispatch(RedisCli redisCli) { + return new RedisMessageDispatch(redisCli); + } + + /** + * 配置 Spring 事件监听器,当触发 Spring 容器刷新事件{@link ContextRefreshedEvent}时,注册自定义的 Redis 监听器 + * + * @param redisMessageDispatch 消息派发器 + * @return {@link ApplicationListener} + * @see RedisListener + * @see RedisChannel + */ + @Bean + public ApplicationListener applicationListener(RedisMessageDispatch redisMessageDispatch) { + return event -> { + Map beansWithAnnotation = event + .getApplicationContext() + .getBeansWithAnnotation(RedisListener.class); + beansWithAnnotation.forEach((k, v) -> { + if (log.isDebugEnabled()) { + log.debug("发现 Redis 监听器:【{}】", v.getClass().getName()); + } + redisMessageDispatch.subscribe(v); + }); + }; + } + } + +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java new file mode 100644 index 0000000..49066d5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisFastjsonCodec.java @@ -0,0 +1,63 @@ +package com.njzscloud.common.redis.support; + +import com.njzscloud.common.core.fastjson.Fastjson; +import io.lettuce.core.codec.RedisCodec; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Redis 序列化/反序列化 器(采用 Fastjson,已开启自动类型处理) + */ +public class RedisFastjsonCodec implements RedisCodec { + private static final byte[] EMPTY = new byte[0]; + + @Override + public String decodeKey(ByteBuffer bytes) { + return Unpooled.wrappedBuffer(bytes).toString(StandardCharsets.UTF_8); + } + + @Override + public Object decodeValue(ByteBuffer bytes) { + String jsonStr = Unpooled.wrappedBuffer(bytes).toString(StandardCharsets.UTF_8); + return Fastjson.toBean(jsonStr, Object.class); + } + + @Override + public ByteBuffer encodeKey(String key) { + if (key == null) { + return ByteBuffer.wrap(EMPTY); + } + int utf8MaxBytes = ByteBufUtil.utf8MaxBytes(key); + + ByteBuffer buffer = ByteBuffer.allocate(utf8MaxBytes); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); + byteBuf.clear(); + ByteBufUtil.writeUtf8(byteBuf, key); + buffer.limit(byteBuf.writerIndex()); + + return buffer; + } + + @Override + public ByteBuffer encodeValue(Object value) { + String jsonStr = Fastjson.toJsonStr(value); + if (jsonStr == null) { + return ByteBuffer.wrap(EMPTY); + } + int utf8MaxBytes = ByteBufUtil.utf8MaxBytes(jsonStr); + + ByteBuffer buffer = ByteBuffer.allocate(utf8MaxBytes); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); + byteBuf.clear(); + ByteBufUtil.writeUtf8(byteBuf, jsonStr); + buffer.limit(byteBuf.writerIndex()); + + return buffer; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java new file mode 100644 index 0000000..f6deeb2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisListenerRegistrar.java @@ -0,0 +1,98 @@ +package com.njzscloud.common.redis.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.redis.annotation.RedisListener; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.*; + +/** + * Redis 监听器注册器, 暂未使用 + */ +@Slf4j +@Setter +public class RedisListenerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { + + private ResourceLoader resourceLoader; + + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + LinkedHashSet candidateComponents = new LinkedHashSet<>(); + + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(RedisListener.class); + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + scanner.addIncludeFilter(annotationTypeFilter); + + Set basePackages = getBasePackages(metadata); + for (String basePackage : basePackages) { + basePackage = basePackage.trim(); + candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); + } + + for (BeanDefinition candidateComponent : candidateComponents) { + String beanClassName = candidateComponent.getBeanClassName(); + if (StrUtil.isBlank(beanClassName)) { + log.warn("当前组件不合法: [{}]", beanClassName); + continue; + } + if (registry.containsBeanDefinition(beanClassName)) { + log.debug("当前组件已存在: [{}]", beanClassName); + continue; + } + if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) { + + AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); + if (annotationMetadata.isInterface() || annotationMetadata.isAbstract()) continue; + + String name = beanDefinition.getBeanClassName(); + registry.registerBeanDefinition(Objects.requireNonNull(name), beanDefinition); + } + } + } + + + protected Set getBasePackages(AnnotationMetadata importingClassMetadata) { + Map attributes = importingClassMetadata.getAnnotationAttributes(RedisListener.class.getCanonicalName()); + + Set basePackages = new HashSet<>(); + if (CollUtil.isNotEmpty(attributes)) { + for (String pkg : (String[]) attributes.get("value")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + } + + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + return basePackages; + } + + protected ClassPathScanningCandidateComponentProvider getScanner() { + return new ClassPathScanningCandidateComponentProvider(false, this.environment) { + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation(); + } + }; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java new file mode 100644 index 0000000..6e8ee16 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/support/RedisMessageDispatch.java @@ -0,0 +1,157 @@ +package com.njzscloud.common.redis.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.redis.RedisCli; +import com.njzscloud.common.redis.annotation.RedisChannel; +import io.lettuce.core.pubsub.RedisPubSubListener; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * Redis 发布订阅模式,消息派发器 + */ +@Slf4j +public class RedisMessageDispatch implements RedisPubSubListener { + private final RedisCli redisCli; + + /** + * 频道/频道模式 —— 监听器对象,消息处理方法 + */ + Map>> dispatchList = new HashMap<>(); + + /** + * 构建消息派发器 + * + * @param redisCli Redis 客户端 {@link RedisCli} + */ + public RedisMessageDispatch(RedisCli redisCli) { + this.redisCli = redisCli; + this.redisCli.addListener(this); + } + + @Override + public void message(String channel, Object message) { + message(null, channel, message); + } + + @Override + public void message(String pattern, String channel, Object message) { + if (log.isDebugEnabled()) { + log.debug("收到 Redis 订阅消息,订阅模式:【{}】、订阅频道:【{}】、消息内容:【{}】", pattern, channel, Fastjson.toJsonStr(message)); + } + + List> methodList; + + if (pattern == null) methodList = dispatchList.get(channel); + else methodList = dispatchList.get(pattern); + + if (methodList == null || methodList.isEmpty()) return; + + for (Tuple2 tuple2 : methodList) { + Object target = tuple2.get_0(); + Method method = tuple2.get_1(); + + int parameterCount = method.getParameterCount(); + Object[] parameters = new Object[parameterCount]; + + if (parameterCount == 1) { + parameters[0] = message; + } else if (parameterCount == 2) { + parameters[0] = channel; + parameters[1] = message; + } else if (parameterCount == 3) { + parameters[0] = pattern; + parameters[1] = channel; + parameters[2] = message; + } + + CompletableFuture.runAsync(() -> { + try { + method.invoke(target, parameters); + } catch (Exception e) { + log.error("Redis 订阅消息处理失败,订阅模式:【{}】、订阅频道:【{}】、消息内容:【{}】", pattern, channel, Fastjson.toJsonStr(message), e); + } + }); + } + } + + @Override + public void subscribed(String channel, long count) { + log.info("订阅,频道:{}、订阅数:{}", channel, count); + } + + @Override + public void psubscribed(String pattern, long count) { + log.info("订阅,模式:{}、订阅数:{}", pattern, count); + } + + @Override + public void unsubscribed(String channel, long count) { + log.info("取消订阅,频道:{}、剩余订阅数:{}", channel, count); + } + + @Override + public void punsubscribed(String pattern, long count) { + log.info("取消订阅,模式:{}、剩余订阅数:{}", pattern, count); + } + + public void subscribe(Object listener) { + Class clazz = listener.getClass(); + HashSet exclude = CollUtil.newHashSet("wait", "equals", "toString", "hashCode", "getClass", "notify", "notifyAll"); + Method[] methods = ClassUtil.getPublicMethods(clazz, it -> { + int modifiers = it.getModifiers(); + int parameterCount = it.getParameterCount(); + String name = it.getName(); + return !exclude.contains(name) && !Modifier.isStatic(modifiers) && !Modifier.isAbstract(modifiers) && (parameterCount == 0 || parameterCount == 1 || parameterCount == 3); + }).toArray(new Method[0]); + if (methods.length == 0) return; + + Set registrationPatterns = new HashSet<>(); + Set registrationChannels = new HashSet<>(); + + String clazzName = clazz.getName(); + for (Method method : methods) { + RedisChannel redisChannel = method.getAnnotation(RedisChannel.class); + if (redisChannel == null) continue; + + Tuple2 listenerTuple = Tuple2.of(listener, method); + + registrationPatterns.addAll(resolveListener(listenerTuple, redisChannel.patterns(), dispatchList)); + + registrationChannels.addAll(resolveListener(listenerTuple, redisChannel.channels(), dispatchList)); + + if (log.isDebugEnabled()) { + log.debug("Redis 监听器,事件处理方法已注册:【{}#{}】", clazzName, method.getName()); + } + } + + this.redisCli.psubscribe(registrationPatterns); + this.redisCli.subscribe(registrationChannels); + } + + /** + * 计算要订阅的频道或频道模式 + */ + private Set resolveListener(Tuple2 listener, String[] channels, Map>> dispatchList) { + if (channels == null || channels.length == 0) return Collections.emptySet(); + + Set channelSet = Arrays.stream(channels).collect(Collectors.toSet()); + + Set registrations = new HashSet<>(channelSet); + + for (String channel : channelSet) { + List> listeners = dispatchList.computeIfAbsent(channel, k -> new ArrayList<>()); + listeners.add(listener); + } + + return registrations; + } +} diff --git a/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java new file mode 100644 index 0000000..402df4f --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/java/com/njzscloud/common/redis/util/Redis.java @@ -0,0 +1,759 @@ +package com.njzscloud.common.redis.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.redis.RedisCli; +import io.lettuce.core.*; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.output.KeyStreamingChannel; +import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.output.ScoredValueStreamingChannel; +import io.lettuce.core.output.ValueStreamingChannel; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; + +/** + * Redis 工具 + */ +@Slf4j +@SuppressWarnings("unchecked") +public final class Redis { + + private final static RedisCli REDIS_CLI; + + static { + REDIS_CLI = SpringUtil.getBean(RedisCli.class); + } + + // region db/key + + /** + * 切换数据库 + * + * @param db 数据库索引 + * @return boolean + * @see RedisCommands#select(int) + */ + public static boolean select(int db) { + return REDIS_CLI.exec(it -> { + return "OK".equalsIgnoreCase(it.select(db)); + }); + } + + /** + * 向指定通道发送消息 + * + * @param channel 通道 + * @param message 消息 + * @return Long 接收消息的客户端数 + */ + public static Long publish(String channel, Object message) { + return REDIS_CLI.exec(it -> { + return it.publish(channel, message); + }); + } + + public static List keys(String pattern) { + return REDIS_CLI.exec(it -> { + return it.keys(pattern); + }); + } + + public static String rename(String key, String newKey) { + return REDIS_CLI.exec(it -> { + return it.rename(key, newKey); + }); + } + + public static Boolean renamenx(String key, String newKey) { + return REDIS_CLI.exec(it -> { + return it.renamenx(key, newKey); + }); + } + + public static Long del(String... keys) { + return REDIS_CLI.exec(it -> { + return it.del(keys); + }); + } + + public static Long exists(String... keys) { + return REDIS_CLI.exec(it -> { + return it.exists(keys); + }); + } + + public static Boolean expire(String key, long seconds) { + return REDIS_CLI.exec(it -> { + return it.expire(key, seconds); + }); + } + + public static Boolean expire(String key, Duration seconds) { + return REDIS_CLI.exec(it -> { + return it.expire(key, seconds); + }); + } + + public static Boolean expireat(String key, long timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + + public static Boolean expireat(String key, Date timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + + public static Boolean expireat(String key, Instant timestamp) { + return REDIS_CLI.exec(it -> { + return it.expireat(key, timestamp); + }); + } + // endregion + + // region str + + public static V get(String key) { + return REDIS_CLI.exec(it -> (V) it.get(key)); + } + + public static String set(String key, Object value, SetArgs setArgs) { + return REDIS_CLI.exec(it -> { + return it.set(key, value, setArgs); + }); + } + + public static String set(String key, Object value) { + return REDIS_CLI.exec(it -> { + return it.set(key, value); + }); + } + + public static String setex(String key, long seconds, Object value) { + return REDIS_CLI.exec(it -> { + return it.setex(key, seconds, value); + }); + } + + public static Boolean setnx(String key, Object value) { + return REDIS_CLI.exec(it -> { + return it.setnx(key, value); + }); + } + + public static List> mget(String... keys) { + return REDIS_CLI.exec(it -> { + return it.mget(keys); + }); + } + + public static Long mget(KeyValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.mget(channel, keys); + }); + } + + public static String mset(Map map) { + return REDIS_CLI.exec(it -> { + return it.mset(map); + }); + } + + public static Boolean msetnx(Map map) { + return REDIS_CLI.exec(it -> { + return it.msetnx(map); + }); + } + + public static Long incr(String key) { + return REDIS_CLI.exec(it -> { + return it.incr(key); + }); + } + + public static Long incrby(String key, long amount) { + return REDIS_CLI.exec(it -> { + return it.incrby(key, amount); + }); + } + + public static Long decr(String key) { + return REDIS_CLI.exec(it -> { + return it.decr(key); + }); + } + + public static Long decrby(String key, long amount) { + return REDIS_CLI.exec(it -> { + return it.decrby(key, amount); + }); + } + + public static Long strlen(String key) { + return REDIS_CLI.exec(it -> { + return it.strlen(key); + }); + } + + + public static Long bitcount(String key) { + return REDIS_CLI.exec(it -> { + return it.bitcount(key); + }); + } + + public static Long bitcount(String key, long start, long end) { + return REDIS_CLI.exec(it -> { + return it.bitcount(key, start, end); + }); + } + + public static List bitfield(String key, BitFieldArgs bitFieldArgs) { + return REDIS_CLI.exec(it -> { + return it.bitfield(key, bitFieldArgs); + }); + } + + public static Long bitpos(String key, boolean state) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state); + }); + } + + public static Long bitpos(String key, boolean state, long start) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state, start); + }); + } + + public static Long bitpos(String key, boolean state, long start, long end) { + return REDIS_CLI.exec(it -> { + return it.bitpos(key, state, start, end); + }); + } + + public static Long bitopAnd(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopAnd(destination, keys); + }); + } + + public static Long bitopNot(String destination, String source) { + return REDIS_CLI.exec(it -> { + return it.bitopNot(destination, source); + }); + } + + public static Long bitopOr(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopOr(destination, keys); + }); + } + + public static Long bitopXor(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.bitopXor(destination, keys); + }); + } + // endregion + + // region hash + public static Long hdel(String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hdel(key, fields); + }); + } + + public static Boolean hexists(String key, String field) { + return REDIS_CLI.exec(it -> { + return it.hexists(key, field); + }); + } + + public static V hget(String key, String field) { + return REDIS_CLI.exec(it -> (V) it.hget(key, field)); + } + + public static Map hgetall(String key) { + return REDIS_CLI.exec(it -> { + return it.hgetall(key); + }); + } + + public static Long hgetall(KeyValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hgetall(channel, key); + }); + } + + public static List hkeys(String key) { + return REDIS_CLI.exec(it -> { + return it.hkeys(key); + }); + } + + public static Long hkeys(KeyStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hkeys(channel, key); + }); + } + + public static Long hlen(String key) { + return REDIS_CLI.exec(it -> { + return it.hlen(key); + }); + } + + public static List> hmget(String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hmget(key, fields); + }); + } + + public static Long hmget(KeyValueStreamingChannel channel, String key, String... fields) { + return REDIS_CLI.exec(it -> { + return it.hmget(channel, key, fields); + }); + } + + public static String hmset(String key, Map map) { + return REDIS_CLI.exec(it -> { + return it.hmset(key, map); + }); + } + + public static Boolean hset(String key, String field, Object value) { + return REDIS_CLI.exec(it -> { + return it.hset(key, field, value); + }); + } + + public static Long hset(String key, Map map) { + return REDIS_CLI.exec(it -> { + return it.hset(key, map); + }); + } + + public static Boolean hsetnx(String key, String field, Object value) { + return REDIS_CLI.exec(it -> { + return it.hsetnx(key, field, value); + }); + } + + public static Long hstrlen(String key, String field) { + return REDIS_CLI.exec(it -> { + return it.hstrlen(key, field); + }); + } + + public static List hvals(String key) { + return REDIS_CLI.exec(it -> { + return it.hvals(key); + }); + } + + public static Long hvals(ValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.hvals(channel, key); + }); + } + // endregion + + // region list + public static V lindex(String key, long index) { + return REDIS_CLI.exec(it -> (V) it.lindex(key, index)); + } + + + public static Long llen(String key) { + return REDIS_CLI.exec(it -> { + return it.llen(key); + }); + } + + public static V lpop(String key) { + return REDIS_CLI.exec(it -> (V) it.lpop(key)); + } + + public static List lpop(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.lpop(key, count)); + } + + public static Long lpush(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.lpush(key, values); + }); + } + + public static Long lpushx(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.lpushx(key, values); + }); + } + + public static List lrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.lrange(key, start, stop)); + } + + public static Long lrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.lrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static Long lrem(String key, long count, V value) { + return REDIS_CLI.exec(it -> { + return it.lrem(key, count, value); + }); + } + + public static String lset(String key, long index, V value) { + return REDIS_CLI.exec(it -> { + return it.lset(key, index, value); + }); + } + + public static String ltrim(String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.ltrim(key, start, stop); + }); + } + + public static V rpop(String key) { + return REDIS_CLI.exec(it -> (V) it.rpop(key)); + } + + public static List rpop(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.rpop(key, count)); + } + + public static Long rpush(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.rpush(key, values); + }); + } + + public static Long rpushx(String key, V... values) { + return REDIS_CLI.exec(it -> { + return it.rpushx(key, values); + }); + } + // endregion + + // region set + public static Long sadd(String key, V... members) { + return REDIS_CLI.exec(it -> { + return it.sadd(key, members); + }); + } + + public static Long scard(String key) { + return REDIS_CLI.exec(it -> { + return it.scard(key); + }); + } + + public static Set sdiff(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sdiff(keys)); + } + + public static Long sdiff(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sdiff((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sdiffstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sdiffstore(destination, keys); + }); + } + + public static Set sinter(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sinter(keys)); + } + + public static Long sinter(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sinter((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sinterstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sinterstore(destination, keys); + }); + } + + public static Boolean sismember(String key, V member) { + return REDIS_CLI.exec(it -> { + return it.sismember(key, member); + }); + } + + public static List smismember(String key, V... member) { + return REDIS_CLI.exec(it -> { + return it.smismember(key, member); + }); + } + + public static Set smembers(String key) { + return REDIS_CLI.exec(it -> (Set) it.smembers(key)); + } + + public static Long smembers(ValueStreamingChannel channel, String key) { + return REDIS_CLI.exec(it -> { + return it.smembers((ValueStreamingChannel) channel, key); + }); + } + + public static V spop(String key) { + return REDIS_CLI.exec(it -> (V) it.spop(key)); + } + + public static Set spop(String key, long count) { + return REDIS_CLI.exec(it -> (Set) it.spop(key, count)); + } + + public static V srandmember(String key) { + return REDIS_CLI.exec(it -> (V) it.srandmember(key)); + } + + public static List srandmember(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.srandmember(key, count)); + } + + public static Long srandmember(ValueStreamingChannel channel, String key, long count) { + return REDIS_CLI.exec(it -> { + return it.srandmember((ValueStreamingChannel) channel, key, count); + }); + } + + public static Long srem(String key, V... members) { + return REDIS_CLI.exec(it -> { + return it.srem(key, members); + }); + } + + public static Set sunion(String... keys) { + return REDIS_CLI.exec(it -> (Set) it.sunion(keys)); + } + + public static Long sunion(ValueStreamingChannel channel, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sunion((ValueStreamingChannel) channel, keys); + }); + } + + public static Long sunionstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.sunionstore(destination, keys); + }); + } + + // endregion + + // region sorted set + public static Long zadd(String key, double score, V member) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, score, member); + }); + } + + public static Long zadd(String key, Object... scoresAndValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, scoresAndValues); + }); + } + + public static Long zadd(String key, ScoredValue... scoredValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, (ScoredValue[]) scoredValues); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, double score, V member) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, score, member); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, Object... scoresAndValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, scoresAndValues); + }); + } + + public static Long zadd(String key, ZAddArgs zAddArgs, ScoredValue... scoredValues) { + return REDIS_CLI.exec(it -> { + return it.zadd(key, zAddArgs, (ScoredValue[]) scoredValues); + }); + } + + public static Long zcard(String key) { + return REDIS_CLI.exec(it -> { + return it.zcard(key); + }); + } + + public static Long zcount(String key, Range range) { + return REDIS_CLI.exec(it -> { + return it.zcount(key, range); + }); + } + + public static List zdiff(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zdiff(keys)); + } + + public static Long zdiffstore(String destKey, String... srcKeys) { + return REDIS_CLI.exec(it -> { + return it.zdiffstore(destKey, srcKeys); + }); + } + + public static List zinter(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zinter(keys)); + } + + public static List zinter(ZAggregateArgs aggregateArgs, String... keys) { + return REDIS_CLI.exec(it -> (List) it.zinter(aggregateArgs, keys)); + } + + public static Long zinterstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zinterstore(destination, keys); + }); + } + + public static Long zinterstore(String destination, ZStoreArgs storeArgs, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zinterstore(destination, storeArgs, keys); + }); + } + + public static V zrandmember(String key) { + return REDIS_CLI.exec(it -> (V) it.zrandmember(key)); + } + + public static List zrandmember(String key, long count) { + return REDIS_CLI.exec(it -> (List) it.zrandmember(key, count)); + } + + public static ScoredValue zrandmemberWithScores(String key) { + return REDIS_CLI.exec(it -> (ScoredValue) it.zrandmemberWithScores(key)); + } + + public static List> zrandmemberWithScores(String key, long count) { + List> scoredValues = REDIS_CLI.exec(it -> it.zrandmemberWithScores(key, count)); + + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + + return list; + } + + public static List zrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.zrange(key, start, stop)); + } + + public static Long zrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List> zrangeWithScores(String key, long start, long stop) { + List> scoredValues = REDIS_CLI.exec(it -> it.zrangeWithScores(key, start, stop)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + + return list; + } + + public static Long zrangeWithScores(ScoredValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrangeWithScores((ScoredValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List zrevrange(String key, long start, long stop) { + return REDIS_CLI.exec(it -> (List) it.zrevrange(key, start, stop)); + } + + public static Long zrevrange(ValueStreamingChannel channel, String key, long start, long stop) { + return REDIS_CLI.exec(it -> { + return it.zrevrange((ValueStreamingChannel) channel, key, start, stop); + }); + } + + public static List zunion(String... keys) { + return REDIS_CLI.exec(it -> (List) it.zunion(keys)); + } + + public static List zunion(ZAggregateArgs aggregateArgs, String... keys) { + return REDIS_CLI.exec(it -> (List) it.zunion(aggregateArgs, keys)); + } + + public static List> zunionWithScores(ZAggregateArgs aggregateArgs, String... keys) { + List> scoredValues = REDIS_CLI.exec(it -> it.zunionWithScores(aggregateArgs, keys)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + return list; + } + + public static List> zunionWithScores(String... keys) { + List> scoredValues = REDIS_CLI.exec(it -> it.zunionWithScores(keys)); + if (scoredValues == null || scoredValues.isEmpty()) return CollUtil.empty(null); + + ArrayList> list = new ArrayList<>(scoredValues.size()); + for (ScoredValue scoredValue : scoredValues) { + double score = scoredValue.getScore(); + Object value = scoredValue.getValue(); + ScoredValue val = (ScoredValue) ScoredValue.fromNullable(score, (V) value); + list.add(val); + } + return list; + } + + public static Long zunionstore(String destination, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zunionstore(destination, keys); + }); + } + + public static Long zunionstore(String destination, ZStoreArgs storeArgs, String... keys) { + return REDIS_CLI.exec(it -> { + return it.zunionstore(destination, storeArgs, keys); + }); + } + // endregion +} + diff --git a/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e6cd518 --- /dev/null +++ b/njzscloud-common/njzscloud-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.redis.config.RedisServiceAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-security/pom.xml b/njzscloud-common/njzscloud-common-security/pom.xml new file mode 100644 index 0000000..3f6b67b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-security + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-wechat + provided + + + com.njzscloud + njzscloud-common-mvc + provided + + + + + + org.springframework.boot + spring-boot-starter-security + + + + diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java new file mode 100644 index 0000000..db2acc0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityAutoConfiguration.java @@ -0,0 +1,189 @@ +package com.njzscloud.common.security.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.njzscloud.common.security.handler.AccessDeniedExceptionHandler; +import com.njzscloud.common.security.handler.AuthExceptionHandler; +import com.njzscloud.common.security.handler.LogoutPostHandler; +import com.njzscloud.common.security.module.code.CodeAuthenticationProvider; +import com.njzscloud.common.security.module.code.CodeLoginPreparer; +import com.njzscloud.common.security.module.password.PasswordAuthenticationProvider; +import com.njzscloud.common.security.module.password.PasswordLoginPreparer; +import com.njzscloud.common.security.module.wechat.mini.WechatMiniAuthenticationProvider; +import com.njzscloud.common.security.module.wechat.mini.WechatMiniLoginPreparer; +import com.njzscloud.common.security.permission.DefaultPermissionManager; +import com.njzscloud.common.security.permission.PermissionManager; +import com.njzscloud.common.security.support.*; +import com.njzscloud.common.security.support.controller.VerificationCodeController; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.njzscloud.common.security.contant.Constants.LOGOUT_URL; + +@Slf4j +@Configuration +@EnableConfigurationProperties({WebSecurityProperties.class}) +public class WebSecurityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(LoginHistoryRecorder.class) + public LoginHistoryRecorder loginHistoryRecorder() { + return new DefaultLoginHistoryRecorder(); + } + + @Bean + @ConditionalOnMissingBean(PermissionManager.class) + public PermissionManager permissionManager() { + return new DefaultPermissionManager(); + } + + @Bean + @ConditionalOnMissingBean({IAuthService.class}) + public DefaultAuthenticationProvider defaultAuthenticationProvider() { + return new DefaultAuthenticationProvider(); + } + + @Bean + @ConditionalOnBean({IAuthService.class}) + public PasswordAuthenticationProvider passwordAuthenticationProvider(IAuthService iAuthService) { + return new PasswordAuthenticationProvider(iAuthService); + } + + @Bean + @ConditionalOnBean({IAuthService.class}) + public PasswordLoginPreparer passwordLoginPreparer() { + return new PasswordLoginPreparer(); + } + + @Bean + @ConditionalOnBean({IAuthService.class}) + public WechatMiniAuthenticationProvider wechatMiniAuthenticationProvider(IAuthService iAuthService) { + return new WechatMiniAuthenticationProvider(iAuthService); + } + + @Bean + @ConditionalOnBean({IAuthService.class}) + public WechatMiniLoginPreparer wechatMiniLoginPreparer() { + return new WechatMiniLoginPreparer(); + } + + @Bean + public VerificationCodeService verificationCodeService() { + return new VerificationCodeService(true); + } + + @Bean + @ConditionalOnBean({IAuthService.class, VerificationCodeService.class}) + public CodeAuthenticationProvider codeAuthenticationProvider(IAuthService iAuthService, VerificationCodeService verificationCodeService) { + return new CodeAuthenticationProvider(iAuthService, verificationCodeService); + } + + @Bean + @ConditionalOnBean({VerificationCodeService.class}) + public VerificationCodeController verificationCodeController(VerificationCodeService verificationCodeService) { + return new VerificationCodeController(verificationCodeService); + } + + @Bean + public CodeLoginPreparer codeLoginPreparer() { + return new CodeLoginPreparer(); + } + + /** + * SpringSecurity 配置 + */ + @Slf4j + @Configuration + @EnableWebSecurity + @RequiredArgsConstructor + public static class AuthenticationServerConfigurer { + + private final LoginHistoryRecorder loginHistoryRecorder; + + private final WebSecurityProperties webSecurityProperties; + + private final PermissionManager permissionManager; + + private final ObjectProvider loginPreparerObjectProvider; + + private final ObjectProvider abstractAuthenticationProviderObjectProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + List loginPreparers = loginPreparerObjectProvider.orderedStream().collect(Collectors.toList()); + List authenticationProviders = abstractAuthenticationProviderObjectProvider.orderedStream().collect(Collectors.toList()); + ProviderManager providerManager = new ProviderManager(authenticationProviders); + + LogoutPostHandler logoutPostHandler = new LogoutPostHandler(); + return http + + .csrf(AbstractHttpConfigurer::disable) + .anonymous(AbstractHttpConfigurer::disable) + .requestCache(AbstractHttpConfigurer::disable) + .sessionManagement(AbstractHttpConfigurer::disable) + + .securityContext(it -> it.securityContextRepository(new TokenSecurityContextRepository())) + .authorizeHttpRequests(it -> it + .anyRequest() + .access((AuthorizationManager) (authentication, object) -> { + // 获取当前请求路径 + String requestPath = object.getRequest().getRequestURI(); + // 获取当前认证用户 + Authentication auth = authentication.get(); + + + return new AuthorizationDecision(true); + }) + ) + // .addFilter(securityInterceptor) + // 退出登录 + .logout(it -> it.logoutUrl(LOGOUT_URL).addLogoutHandler(logoutPostHandler).logoutSuccessHandler(logoutPostHandler)) + + .with(new CombineAuthenticationConfigurer(), + it -> it.authenticationManager(providerManager) + .loginPreparers(loginPreparers) + .loginHistoryRecorder(loginHistoryRecorder)) + + // 异常处理 + .exceptionHandling(it -> it + .authenticationEntryPoint(AuthExceptionHandler.INSTANCE) + .accessDeniedHandler(AccessDeniedExceptionHandler.INSTANCE)) + .build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> { + WebSecurity.IgnoredRequestConfigurer ignoring = web.ignoring(); + Set authIgnore = webSecurityProperties.getAuthIgnores(); + if (CollUtil.isNotEmpty(authIgnore)) { + ignoring.requestMatchers(ArrayUtil.toArray(authIgnore, String.class)); + } + ignoring.requestMatchers("/error"); + }; + } + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java new file mode 100644 index 0000000..b36d348 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/config/WebSecurityProperties.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.config; + +import cn.hutool.core.collection.CollUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; +import java.util.Set; + +@Getter +@Setter +@ConfigurationProperties(prefix = "spring.security") +public class WebSecurityProperties { + /** + * TOKEN 过期时间, ≤ 0-->永久,默认 0 + */ + private Duration tokenExp = Duration.ofMillis(0); + /** + * 不进行认证校验的路径, 按 Ant 格式匹配 + */ + private Set authIgnores = CollUtil.empty(Set.class); + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java new file mode 100644 index 0000000..1b14045 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/AuthWay.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:auth_way + * 字典名称:登录方式 + */ +@Getter +@RequiredArgsConstructor +public enum AuthWay implements DictStr { + ANONYMOUS("Anonymous", "匿名登录"), + PASSWORD("Password", "账号密码登录"), + PHONE("Phone", "手机验证码登录"), + WECHAT("Wechat", "微信登录"), + WECHAT_MINI("WechatMini", "微信小程序登录"), + ; + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/ClientCode.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/ClientCode.java new file mode 100644 index 0000000..012055d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/ClientCode.java @@ -0,0 +1,31 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.ienum.DictInt; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:client_code + * 字典名称:客户端代码 + */ +@Getter +@RequiredArgsConstructor +public enum ClientCode implements DictInt { + PC(0, "电脑端"), + WX_MINI_APP(1, "微信小程序"), + ; + + private final Integer val; + + private final String txt; + + /** + * 检查是否有登录权限 + */ + public boolean hasPermission(int clientCode) { + var mask = 1 << this.val; + return (clientCode & mask) == 0; + } +} + + diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java new file mode 100644 index 0000000..0687f90 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/Constants.java @@ -0,0 +1,31 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.utils.Key; +import com.njzscloud.common.security.support.UserDetail; + +/** + * 常量 + */ +public final class Constants { + // 随机码字符串池 + public static final String RANDOM_BASE_STRING = "23456789abcdefjhjkmnpqrstuvwxyzABCDEFJHJKMNPQRSTUVWXYZ"; + + public static final String ROLE_AUTHENTICATED = "ROLE_AUTHENTICATED"; + public static final String ROLE_ANONYMOUS = "ROLE_ANONYMOUS"; + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + + // Redis 订阅频道 权限更新 + public static final String REDIS_TOPIC_PERMISSION_UPDATE = "permission_update"; + + public static final Key TOKEN_CACHE_KEY = Key.create("token:{userId}:{tid}"); + public static final String TOKEN_STR_SEPARATOR = ","; + public static final String LOGIN_URL = "/auth/login"; + public static final String LOGIN_CODE_URL = "/auth/login_code"; + public static final String LOGIN_WECHAT_URL = "/auth/login_wechat"; + public static final String LOGOUT_URL = "/auth/logout"; + + /** + * 匿名用户 + */ + public static final UserDetail ANONYMOUS_USER = UserDetail.anonymousUser(); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java new file mode 100644 index 0000000..e71e4c0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/contant/EndpointAccessModel.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.security.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:endpoint_access_model + * 字典名称:接口访问模式 + */ +@Getter +@RequiredArgsConstructor +public enum EndpointAccessModel implements DictStr { + ANONYMOUS("Anonymous", "允许匿名访问"), + LOGINED("Logined", "允许已登录用户访问"), + AUTHENTICATED("Authenticated", "仅拥有权限的用户访问"), + FORBIDDEN("Forbidden", "禁止访问"), + ; + + private final String val; + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java new file mode 100644 index 0000000..be5875e --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/ForbiddenAccessException.java @@ -0,0 +1,19 @@ +package com.njzscloud.common.security.ex; + + +import org.springframework.security.access.AccessDeniedException; + +/** + * 禁止访问 + */ +public class ForbiddenAccessException extends AccessDeniedException { + + public ForbiddenAccessException(String msg) { + super(msg); + } + + public ForbiddenAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java new file mode 100644 index 0000000..fc0ba58 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/MissingPermissionException.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.security.ex; + + +import org.springframework.security.access.AccessDeniedException; + +public class MissingPermissionException extends AccessDeniedException { + + public MissingPermissionException(String msg) { + super(msg); + } + + public MissingPermissionException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java new file mode 100644 index 0000000..da90a0c --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/ex/UserLoginException.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.security.ex; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import org.springframework.security.core.AuthenticationException; + +/** + * 认证异常 + */ +public class UserLoginException extends AuthenticationException { + + public final ExceptionMsg msg; + + public UserLoginException(ExceptionMsg exceptionMsg, String message) { + super(message); + this.msg = exceptionMsg; + } + + public UserLoginException(Throwable cause, ExceptionMsg exceptionMsg, String message, Object... param) { + super(StrUtil.format(message, param), cause); + this.msg = exceptionMsg; + } + + public UserLoginException(Throwable cause, ExceptionMsg exceptionMsg, String message) { + super(message, cause); + this.msg = exceptionMsg; + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java new file mode 100644 index 0000000..37c022b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AccessDeniedExceptionHandler.java @@ -0,0 +1,52 @@ +package com.njzscloud.common.security.handler; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mvc.util.ServletUtil; +import com.njzscloud.common.security.ex.MissingPermissionException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.AuthorizationServiceException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +/** + * 权限异常 + */ +@Slf4j +public class AccessDeniedExceptionHandler implements AccessDeniedHandler { + + public static final AccessDeniedExceptionHandler INSTANCE = new AccessDeniedExceptionHandler(); + + private AccessDeniedExceptionHandler() { + } + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException, ServletException { + R r; + if (accessDeniedException instanceof AuthorizationServiceException) { + log.error("权限校验失败: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "权限加载失败"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else if (accessDeniedException instanceof MissingPermissionException) { + log.error("权限未配置: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "当前请求未分配权限"); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else { + log.error("暂无权限: {}", request.getRequestURI(), accessDeniedException); + r = R.failed(ExceptionMsg.CLI_ERR_MSG, "暂无权限"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java new file mode 100644 index 0000000..a0f7518 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/AuthExceptionHandler.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.security.handler; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mvc.util.ServletUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + + +/** + * 认证异常 + */ +@Slf4j +public class AuthExceptionHandler implements AuthenticationEntryPoint { + public static final AuthExceptionHandler INSTANCE = new AuthExceptionHandler(); + + private AuthExceptionHandler() { + } + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException, ServletException { + log.error("未登录: {}", request.getRequestURI(), authException); + R r; + if (authException instanceof AuthenticationCredentialsNotFoundException) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + r = R.failed(ExceptionMsg.CLI_ERR_MSG, "登录凭证无效"); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "无法进行用户校验"); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java new file mode 100644 index 0000000..a389534 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LoginPostHandler.java @@ -0,0 +1,97 @@ +package com.njzscloud.common.security.handler; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mvc.util.ServletUtil; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.AuthenticationDetails; +import com.njzscloud.common.security.support.LoginHistory; +import com.njzscloud.common.security.support.LoginHistoryRecorder; +import com.njzscloud.common.security.support.UserDetail; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; + +/** + * 登陆后置处理器 + */ +@Slf4j +@RequiredArgsConstructor +public class LoginPostHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { + + private final LoginHistoryRecorder loginHistoryRecorder; + + /** + * 登陆失败 + */ + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + log.error("登录失败", exception); + R r; + if (exception instanceof UserLoginException) { + r = R.failed(((UserLoginException) exception).msg, exception.getMessage()); + } else if (exception instanceof UsernameNotFoundException) { + r = R.failed(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误"); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + r = R.failed(ExceptionMsg.SYS_ERR_MSG, "无法进行用户校验"); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + /** + * 登陆成功 + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + R r = R.success(authentication.getPrincipal()); + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + /** + * 记录登陆日志 + * + * @param authentication 已认证的认证对象 + */ + public void recordLoginHistory(Authentication authentication) { + AuthenticationDetails details = (AuthenticationDetails) authentication.getDetails(); + UserDetail userDetail = (UserDetail) authentication.getPrincipal(); + AuthWay authWay = userDetail.getAuthWay(); + + long userAccountId = userDetail.getAccountId(); + long userId = userDetail.getUserId(); + LocalDateTime now = LocalDateTime.now(); + LoginHistory loginHistory = new LoginHistory() + .setUserId(userId) + .setAccountId(userAccountId) + .setLoginTime(now) + .setAuthWay(authWay) + .setIp(details.getRemoteAddress()) + .setUserAgent(details.getUserAgent()); + CompletableFuture.runAsync(() -> { + try { + loginHistoryRecorder.record(loginHistory); + } catch (Throwable e) { + log.error("登录日志记录失败", e); + } + }); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java new file mode 100644 index 0000000..5ee84c1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/handler/LogoutPostHandler.java @@ -0,0 +1,54 @@ +package com.njzscloud.common.security.handler; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.core.utils.Mime; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mvc.util.ServletUtil; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserAuthenticationToken; +import com.njzscloud.common.security.util.SecurityUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import java.io.IOException; + +/** + * 退出登陆处理器 + */ +public class LogoutPostHandler implements LogoutSuccessHandler, LogoutHandler { + + /** + * 退出登陆 + */ + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + Token token = (Token) userAuthenticationToken.getCredentials(); + SecurityUtil.removeToken(token); + } + + /** + * 退出登陆成功 + */ + @Override + public void onLogoutSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + R r; + if (authentication == null) { + r = R.failed(Boolean.FALSE, ExceptionMsg.CLI_ERR_MSG, "未登陆,无需操作"); + } else { + r = R.success(Boolean.TRUE); + } + ServletUtil.write(response, Jackson.toJsonStr(r), Mime.u8Val(Mime.JSON)); + } + + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeAuthenticationProvider.java new file mode 100644 index 0000000..7bed695 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeAuthenticationProvider.java @@ -0,0 +1,59 @@ +package com.njzscloud.common.security.module.code; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.support.*; +import lombok.RequiredArgsConstructor; + +import java.util.Set; + +/** + * 验证码登录认证器 + */ +@RequiredArgsConstructor +public class CodeAuthenticationProvider extends AbstractAuthenticationProvider { + private final IAuthService iAuthService; + private final VerificationCodeService verificationCodeService; + + /** + * 读取用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + @Override + protected UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException { + CodeLoginForm codeLoginForm = (CodeLoginForm) loginForm; + String phone = codeLoginForm.getPhone(); + UserDetail userDetail = iAuthService.selectUser(codeLoginForm); + if (userDetail == null) throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "账号不存在"); + Long userId = userDetail.getUserId(); + Set roles = iAuthService.selectRole(userId); + return userDetail + .setAuthWay(AuthWay.PASSWORD) + .setRoles(roles) + ; + } + + @Override + protected void afterCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + CodeLoginForm codeLoginForm = (CodeLoginForm) loginForm; + String codeId = codeLoginForm.getCodeId(); + String code = codeLoginForm.getCode(); + Assert.isTrue(verificationCodeService.checkCode(codeId, code), () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "验证码错误")); + } + + /** + * 获取登录表单类型 + * + * @return 登录表单类型 + * @see PasswordLoginForm + */ + @Override + protected Class getLoginFormClazz() { + return CodeLoginForm.class; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginForm.java new file mode 100644 index 0000000..050d883 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginForm.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.security.module.code; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.support.LoginForm; +import lombok.Getter; +import lombok.Setter; + +/** + * 登录参数 + */ +@Getter +@Setter +public class CodeLoginForm extends LoginForm { + /** + * 手机号 + */ + private String phone; + + /** + * 验证码 + */ + private String code; + + /** + * 获取验证码时使用的 uid + */ + private String codeId; + + public CodeLoginForm() { + super(AuthWay.PHONE); + } + + @Override + public String getName() { + return phone; + } + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(phone), "手机号不能为空"), + ValidRule.of(() -> StrUtil.isNotBlank(code), "验证码不能为空"), + ValidRule.of(() -> StrUtil.isNotBlank(codeId), "验证码标识不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginPreparer.java new file mode 100644 index 0000000..1c93985 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/code/CodeLoginPreparer.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.security.module.code; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.LoginPreparer; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +/** + * 验证码登录,登录参数处理器 + */ +public class CodeLoginPreparer implements LoginPreparer { + private static final PathPatternRequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, Constants.LOGIN_CODE_URL); + + /** + * 创建登录参数 + * + * @param request 请求信息 + * @return 登录参数 {@link PasswordLoginForm} + */ + @Override + public LoginForm createLoginForm(HttpServletRequest request) { + try (ServletInputStream inputStream = request.getInputStream()) { + return Jackson.toBean(inputStream, CodeLoginForm.class); + } catch (Exception e) { + throw new UserLoginException(e, ExceptionMsg.SYS_ERR_MSG, "登录表单解析失败"); + } + } + + /** + * 是否支持当前登录方式 + * + * @param request 请求信息 + * @return 当请求方式为 POST 且路径为 /auth/login 时返回 true + */ + @Override + public boolean support(HttpServletRequest request) { + return matcher.matches(request); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java new file mode 100644 index 0000000..fd14fc6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordAuthenticationProvider.java @@ -0,0 +1,67 @@ +package com.njzscloud.common.security.module.password; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.SysThrowable; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.AbstractAuthenticationProvider; +import com.njzscloud.common.security.support.IAuthService; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.EncryptUtil; +import lombok.RequiredArgsConstructor; + +import java.util.Set; + +/** + * 账号密码登录认证器 + */ +@RequiredArgsConstructor +public class PasswordAuthenticationProvider extends AbstractAuthenticationProvider { + private final IAuthService iAuthService; + + /** + * 读取用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + @Override + protected UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException { + PasswordLoginForm passwordLoginForm = (PasswordLoginForm) loginForm; + UserDetail userDetail; + try { + userDetail = iAuthService.selectUser(passwordLoginForm); + } catch (SysThrowable e) { + throw new UserLoginException(e.msg, e.message.toString()); + } + if (userDetail == null) throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误"); + Long userId = userDetail.getUserId(); + Set roles = iAuthService.selectRole(userId); + return userDetail + .setAuthWay(AuthWay.PASSWORD) + .setRoles(roles) + ; + } + + + @Override + protected void afterCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + String secret = userDetail.getSecret(); + Assert.notBlank(secret, () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "当前账号不支持密码登录")); + PasswordLoginForm passwordLoginForm = (PasswordLoginForm) loginForm; + Assert.isTrue(EncryptUtil.matches(passwordLoginForm.getSecret(), secret), () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "账号或密码错误")); + } + + /** + * 获取登录表单类型 + * + * @return 登录表单类型 + * @see PasswordLoginForm + */ + @Override + protected Class getLoginFormClazz() { + return PasswordLoginForm.class; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java new file mode 100644 index 0000000..c690cf8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginForm.java @@ -0,0 +1,53 @@ +package com.njzscloud.common.security.module.password; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.support.LoginForm; +import lombok.Getter; +import lombok.Setter; + +/** + * 登录参数 + */ +@Getter +@Setter +public class PasswordLoginForm extends LoginForm { + /** + * 登录账号 + */ + private String account; + + /** + * 登录密码 + */ + private String secret; + + /** + * 验证码 + */ + private String captcha; + + /** + * 获取验证码时使用的 uid + */ + private String captchaId; + + public PasswordLoginForm() { + super(AuthWay.PASSWORD); + } + + @Override + public String getName() { + return account; + } + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(account), "请填写账号"), + ValidRule.of(() -> StrUtil.isNotBlank(secret), "请填写密码"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java new file mode 100644 index 0000000..70e233c --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/password/PasswordLoginPreparer.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.security.module.password; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.LoginPreparer; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import static com.njzscloud.common.security.contant.Constants.LOGIN_URL; + +/** + * 账号密码登录,登录参数处理器 + */ +@Slf4j +public class PasswordLoginPreparer implements LoginPreparer { + private static final PathPatternRequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, LOGIN_URL); + + /** + * 创建登录参数 + * + * @param request 请求信息 + * @return 登录参数 {@link PasswordLoginForm} + */ + @Override + public LoginForm createLoginForm(HttpServletRequest request) { + try (ServletInputStream inputStream = request.getInputStream()) { + LoginForm loginForm = Jackson.toBean(inputStream, PasswordLoginForm.class); + log.info("账号密码登录表单:{}", Jackson.toJsonStr(loginForm)); + return loginForm; + } catch (Exception e) { + throw new UserLoginException(e, ExceptionMsg.SYS_ERR_MSG, "登录表单解析失败"); + } + } + + /** + * 是否支持当前登录方式 + * + * @param request 请求信息 + * @return 当请求方式为 POST 且路径为 /login 时返回 true + */ + @Override + public boolean support(HttpServletRequest request) { + return matcher.matches(request); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniAuthenticationProvider.java new file mode 100644 index 0000000..1a9474c --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniAuthenticationProvider.java @@ -0,0 +1,57 @@ +package com.njzscloud.common.security.module.wechat.mini; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.ex.SysThrowable; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.support.AbstractAuthenticationProvider; +import com.njzscloud.common.security.support.IAuthService; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.UserDetail; +import lombok.RequiredArgsConstructor; + +import java.util.Set; + +/** + * 验证码登录认证器 + */ +@RequiredArgsConstructor +public class WechatMiniAuthenticationProvider extends AbstractAuthenticationProvider { + private final IAuthService iAuthService; + + /** + * 读取用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + @Override + protected UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException { + WechatMiniLoginForm wechatMiniLoginForm = (WechatMiniLoginForm) loginForm; + UserDetail userDetail; + try { + userDetail = iAuthService.selectUser(wechatMiniLoginForm); + } catch (SysThrowable e) { + throw new UserLoginException(e.msg, e.message.toString()); + } + if (userDetail == null) throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "微信登录失败"); + Long userId = userDetail.getUserId(); + Set roles = iAuthService.selectRole(userId); + return userDetail + .setAuthWay(AuthWay.WECHAT_MINI) + .setRoles(roles) + ; + } + + /** + * 获取登录表单类型 + * + * @return 登录表单类型 + * @see PasswordLoginForm + */ + @Override + protected Class getLoginFormClazz() { + return WechatMiniLoginForm.class; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginForm.java new file mode 100644 index 0000000..f1b2423 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginForm.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.security.module.wechat.mini; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.support.LoginForm; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class WechatMiniLoginForm extends LoginForm { + /** + * 验证码 + */ + private String code; + private String openid; + private String unionid; + + public WechatMiniLoginForm() { + super(AuthWay.WECHAT_MINI); + } + + @Override + public String getName() { + return code; + } + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(code), "微信验证码不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginPreparer.java new file mode 100644 index 0000000..fcc3020 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/module/wechat/mini/WechatMiniLoginPreparer.java @@ -0,0 +1,68 @@ +package com.njzscloud.common.security.module.wechat.mini; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.jackson.Jackson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.ex.UserLoginException; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.support.LoginForm; +import com.njzscloud.common.security.support.LoginPreparer; +import com.njzscloud.common.wechat.WechatUtil; +import com.njzscloud.common.wechat.param.Code2SessionParam; +import com.njzscloud.common.wechat.result.Code2SessionResult; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +/** + * 验证码登录,登录参数处理器 + */ +@Slf4j +public class WechatMiniLoginPreparer implements LoginPreparer { + private static final PathPatternRequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, Constants.LOGIN_WECHAT_URL); + + /** + * 创建登录参数 + * + * @param request 请求信息 + * @return 登录参数 {@link PasswordLoginForm} + */ + @Override + public LoginForm createLoginForm(HttpServletRequest request) { + try (ServletInputStream inputStream = request.getInputStream()) { + WechatMiniLoginForm loginForm = Jackson.toBean(inputStream, WechatMiniLoginForm.class); + log.info("微信小程序登录表单:{}", Jackson.toJsonStr(loginForm)); + String code = loginForm.getCode(); + if (StrUtil.isBlank(code)) { + throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "微信验证码不能为空"); + } + Code2SessionResult code2SessionResult = WechatUtil.code2Session(new Code2SessionParam().setJs_code(code)); + Integer errcode = code2SessionResult.getErrcode(); + if (errcode != null && errcode != 0) { + log.error("微信登录失败, errcode: {}, errmsg: {}", errcode, code2SessionResult.getErrmsg()); + throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "微信登录失败"); + } + String openid = code2SessionResult.getOpenid(); + String unionid = code2SessionResult.getUnionid(); + return loginForm.setOpenid(openid) + .setUnionid(unionid); + } catch (Exception e) { + throw new UserLoginException(e, ExceptionMsg.SYS_ERR_MSG, "登录表单解析失败"); + } + } + + /** + * 是否支持当前登录方式 + * + * @param request 请求信息 + * @return 当请求方式为 POST 且路径为 /auth/login 时返回 true + */ + @Override + public boolean support(HttpServletRequest request) { + return matcher.matches(request); + } + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java new file mode 100644 index 0000000..1b56dea --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/DefaultPermissionManager.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.security.permission; + +import com.njzscloud.common.security.contant.EndpointAccessModel; + +import java.util.Collections; +import java.util.List; + +/** + * 默认权限管理器
+ * 所有接口都必须登录后才能访问 + */ +public class DefaultPermissionManager extends PermissionManager { + + private final List DEFAULT_ROLE_PERMISSIONS = Collections.singletonList( + new RolePermission() + .setEndpoint("/**") + .setAccessModel(EndpointAccessModel.LOGINED) + ); + + @Override + protected List load() { + return DEFAULT_ROLE_PERMISSIONS; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java new file mode 100644 index 0000000..67bd9f7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionManager.java @@ -0,0 +1,171 @@ +package com.njzscloud.common.security.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.contant.EndpointAccessModel; +import com.njzscloud.common.security.ex.ForbiddenAccessException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 权限管理器 + */ +@Slf4j +public abstract class PermissionManager { + + private static final ReentrantLock PERMISSION_CACHE_LOCK = new ReentrantLock(); + /** + * 权限缓存,请求地址——角色编码 + */ + private Map> PERMISSION_CACHE; + + private Set FORBIDDEN_CACHE; + + /** + * 刷新本地权限缓存 + */ + public final void refresh() { + try { + if (log.isDebugEnabled()) log.debug("刷新本地权限缓存已清除"); + PERMISSION_CACHE_LOCK.lock(); + PERMISSION_CACHE = null; + FORBIDDEN_CACHE = null; + if (log.isDebugEnabled()) log.debug("本地权限缓存已清除"); + this.load0(); + } finally { + PERMISSION_CACHE_LOCK.unlock(); + } + } + + /** + * 初始化本地权限缓存 + */ + public final void init() { + // if (log.isDebugEnabled()) log.debug("初始化本地权限缓存"); + if (CollUtil.isEmpty(PERMISSION_CACHE)) { + try { + PERMISSION_CACHE_LOCK.lock(); + if (CollUtil.isEmpty(PERMISSION_CACHE)) { + this.load0(); + } + } finally { + PERMISSION_CACHE_LOCK.unlock(); + } + } + // if (log.isDebugEnabled()) log.debug("已初始化无需操作"); + } + + /** + * 加载权限 + */ + private void load0() { + // if (log.isDebugEnabled()) log.debug("开始加载权限"); + + List rolePermissions = load(); + + if (rolePermissions == null) rolePermissions = Collections.emptyList(); + + Map> permissionMap = new LinkedHashMap<>(); + + Set forbiddenSet = new HashSet<>(); + + for (RolePermission rolePermission : rolePermissions) { + String endpoint = rolePermission.getEndpoint(); + String method = rolePermission.getMethod(); + EndpointAccessModel accessModel = rolePermission.getAccessModel(); + HttpMethod httpMethod = HttpMethod.valueOf(method); + PathPatternRequestMatcher pathRequestMatcher = PathPatternRequestMatcher.withDefaults().matcher(httpMethod, endpoint); + if (accessModel == EndpointAccessModel.FORBIDDEN) { + forbiddenSet.add(pathRequestMatcher); + continue; + } + + Collection configAttributes = permissionMap.computeIfAbsent(pathRequestMatcher, it -> new HashSet<>()); + + if (accessModel == EndpointAccessModel.ANONYMOUS) { + configAttributes.add(Constants.ROLE_ANONYMOUS); + configAttributes.add(Constants.ROLE_AUTHENTICATED); + } else if (accessModel == EndpointAccessModel.LOGINED) { + configAttributes.add(Constants.ROLE_AUTHENTICATED); + } else if (accessModel == EndpointAccessModel.AUTHENTICATED) { + String role = rolePermission.getRole(); + if (StrUtil.isNotBlank(role)) configAttributes.add(role); + } + } + + FORBIDDEN_CACHE = forbiddenSet; + PERMISSION_CACHE = permissionMap; + + // if (log.isDebugEnabled()) log.debug("本地权限缓存已加载:\n{}", Jackson.toJsonStr(this.getAllRelation())); + } + + /** + * 加载权限 + * + * @return List<RolePermission> + */ + abstract protected List load(); + + /** + * 获取当前请求所需要的角色 + * + * @param request 请求对象 + * @return Collection<String> + */ + public final Collection extractAuthorities(HttpServletRequest request) { + this.init(); + if (FORBIDDEN_CACHE != null) { + for (PathPatternRequestMatcher PathPatternRequestMatcher : FORBIDDEN_CACHE) { + if (PathPatternRequestMatcher.matches(request)) { + throw new ForbiddenAccessException("当前服务已停止使用!"); + } + } + } + if (PERMISSION_CACHE != null) { + for (Map.Entry> entry : PERMISSION_CACHE.entrySet()) { + if (entry.getKey().matches(request)) { + return entry.getValue(); + } + } + } + return CollUtil.empty(Set.class); + } + + /** + * 获取所有角色 + * + * @return 角色列表 + */ + public final Collection getAll() { + this.init(); + Set allAttributes = new HashSet<>(); + if (PERMISSION_CACHE != null) PERMISSION_CACHE.values().forEach(allAttributes::addAll); + return CollUtil.unmodifiable(allAttributes); + } + + /** + * 获取权限的字符表示形式 + * + * @return Map<String, Set<String>> + */ + public synchronized final Map> getAllRelation() { + Map> map = new HashMap<>(); + if (PERMISSION_CACHE != null) { + Set>> entries = PERMISSION_CACHE.entrySet(); + for (Map.Entry> entry : entries) { + PathPatternRequestMatcher key = entry.getKey(); + Collection value = entry.getValue(); + Set collect = new HashSet<>(value); + map.put(key.toString(), collect); + } + } + return MapUtil.unmodifiable(map); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java new file mode 100644 index 0000000..ec3b923 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionSecurityMetaDataSource.java @@ -0,0 +1,43 @@ +package com.njzscloud.common.security.permission; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.security.ex.MissingPermissionException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.web.FilterInvocation; + +import java.util.Collection; + +@Slf4j +@RequiredArgsConstructor +public class PermissionSecurityMetaDataSource { + + // org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer + // org.springframework.security.access.vote.RoleVoter PermissionAuthorizationConfigurer + + private final PermissionManager permissionManager; + // private final boolean rejectPublicInvocations; + + + public Collection getAttributes(Object object) throws IllegalArgumentException { + HttpServletRequest request = ((FilterInvocation) object).getRequest(); + Collection permission = permissionManager.extractAuthorities(request); + String requestURI = request.getRequestURI(); + String method = request.getMethod(); + String endpoint = method.toUpperCase() + " " + requestURI; + + Assert.notEmpty(permission, () -> new MissingPermissionException(StrUtil.format("请求: 【{}】 未指定权限", endpoint))); + // if (log.isDebugEnabled()) log.debug("允许访问接口:【{}】的角色:【{}】", endpoint, permission); + return permission; + } + + public Collection getAllConfigAttributes() { + return permissionManager.getAll(); + } + + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java new file mode 100644 index 0000000..2215062 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/PermissionVoter.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.security.permission; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * 投票器 + */ +public class PermissionVoter { + + int ACCESS_GRANTED = 1; + + int ACCESS_ABSTAIN = 0; + + int ACCESS_DENIED = -1; + + public int vote(Authentication authentication, Object object, Collection attributes) { + if (authentication == null) { + return ACCESS_DENIED; + } + int result = ACCESS_ABSTAIN; + Collection authorities = authentication.getAuthorities(); + for (String attribute : attributes) { + if (this.supports(attribute)) { + result = ACCESS_DENIED; + for (GrantedAuthority authority : authorities) { + if (attribute.equals(authority.getAuthority())) { + return ACCESS_GRANTED; + } + } + } + } + return result; + } + + public boolean supports(String attribute) { + return true; + } + + public boolean supports(Class clazz) { + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java new file mode 100644 index 0000000..be7c374 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/permission/RolePermission.java @@ -0,0 +1,35 @@ +package com.njzscloud.common.security.permission; + +import com.njzscloud.common.security.contant.EndpointAccessModel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 角色权限信息 + */ +@Getter +@Setter +@Accessors(chain = true) +public class RolePermission { + + /** + * 请求方法 + */ + private String method; + + /** + * 端点地址 + */ + private String endpoint; + + /** + * 接口访问模式 + */ + private EndpointAccessModel accessModel; + + /** + * 角色编码 + */ + private String role; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java new file mode 100644 index 0000000..7d13dcf --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AbstractAuthenticationProvider.java @@ -0,0 +1,137 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.security.contant.ClientCode; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.ex.UserLoginException; +import org.springframework.security.authentication.AccountStatusException; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Set; + +/** + * 认证器 + */ +public abstract class AbstractAuthenticationProvider implements AuthenticationProvider { + + /** + * 是否支持当前登录信息 + * + * @param authentication 认证信息类型(在 filter 中调用 authenticate 方法传入的待认证对象的类型) + * @return true-->支持 + */ + @Override + public final boolean supports(Class authentication) { + return UserAuthenticationToken.class.isAssignableFrom(authentication); + } + + @Override + public final Authentication authenticate(Authentication authentication) throws AuthenticationException { + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + + Object principal = userAuthenticationToken.getPrincipal(); + + if (!principal.getClass().isAssignableFrom(this.getLoginFormClazz())) return null; + + LoginForm loginForm = (LoginForm) principal; + + this.beforeCheck(loginForm); + + UserDetail userDetail; + try { + userDetail = this.retrieveUser(loginForm); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + throw new InternalAuthenticationServiceException("服务器异常,用户信息加载失败", e); + } + + Assert.notNull(userDetail, () -> new UsernameNotFoundException("用户不存在")); + + Set roles = userDetail.getRoles(); + if (CollUtil.isEmpty(roles)) { + userDetail.setRoles(CollUtil.newHashSet(Constants.ROLE_ANONYMOUS, Constants.ROLE_AUTHENTICATED)); + } else { + roles.add(Constants.ROLE_ANONYMOUS); + roles.add(Constants.ROLE_AUTHENTICATED); + } + + this.afterCheck(loginForm, userDetail); + + // 敏感信息擦除 + if (loginForm instanceof CredentialsContainer) { + ((CredentialsContainer) loginForm).eraseCredentials(); + } + + this.lastCheck(loginForm, userDetail); + AuthenticationDetails details = (AuthenticationDetails) userAuthenticationToken.getDetails(); + return this.createAuthentication(userDetail, details); + } + + /** + * 创建已完成认证的认证对象 + * + * @param userDetail 用户信息 + * @param details 额外登录信息 + * @return 已完成认证的认证对象 {@link UserAuthenticationToken} + */ + private Authentication createAuthentication(UserDetail userDetail, AuthenticationDetails details) { + Token token = Token.of(userDetail.getUserId(), userDetail.getAccountId(), userDetail.getAuthWay()); + userDetail.setToken(token); + return UserAuthenticationToken.create(userDetail, details); + } + + /** + * 表单检查,在查询用户信息之前执行 + * + * @param loginForm 登录表单 + * @throws UserLoginException 表单校验失败是抛出 + */ + protected void beforeCheck(LoginForm loginForm) throws UserLoginException { + } + + /** + * 用户信息检查(如:密码校验),在查询用户信息之后执行 + * + * @param userDetail 用户信息 + * @throws AccountStatusException 用户信息校验失败时抛出 + */ + protected void afterCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + } + + /** + * 最终检查,用于检查账号状态,如:是否锁定 + * + * @param userDetail 用户信息 + * @throws AccountStatusException 账号状态校验失败时抛出 + */ + protected void lastCheck(LoginForm loginForm, UserDetail userDetail) throws UserLoginException { + Assert.isFalse(userDetail.getDisabled(), () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "用户已被禁用")); + ClientCode clientCode = loginForm.getClientCode(); + Integer code = userDetail.getClientCode(); + Assert.isTrue(clientCode.hasPermission(code), () -> new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "当前用户无权使用:" + clientCode.getTxt())); + } + + /** + * 加载用户信息 + * + * @param loginForm 登录表单 + * @return 用户信息 + */ + protected abstract UserDetail retrieveUser(LoginForm loginForm) throws UserLoginException; + + /** + * 获取登录表单的类型 + * + * @return {@link LoginForm} 的子类 + */ + protected abstract Class getLoginFormClazz(); + +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java new file mode 100644 index 0000000..fe47585 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/AuthenticationDetails.java @@ -0,0 +1,54 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.authentication.AuthenticationDetailsSource; + +/** + *

用户认证详情

+ *

获取用户 IP, Nginx 反向代理 $remote_addr X-Real-IP

+ *

获取用户代理, Nginx 反向代理 $http_user_agent X-Real-UA

+ */ +@Getter +@Setter +@EqualsAndHashCode +public class AuthenticationDetails { + + /** + * 从请求对象中获取额外登录信息, IP 或 用户代理 + */ + private static final AuthenticationDetailsSource AUTHENTICATION_DETAILS_SOURCE = AuthenticationDetails::new; + + + /** + * 用户 IP + */ + private final String remoteAddress; + + /** + * 用户 Http 代理 + */ + private final String userAgent; + + public AuthenticationDetails(HttpServletRequest request) { + String x_real_ip = request.getHeader("X-Real-IP"); + if (StrUtil.isBlank(x_real_ip)) { + this.remoteAddress = request.getRemoteAddr(); + } else { + this.remoteAddress = x_real_ip; + } + String x_real_ua = request.getHeader("X-Real-UA"); + if (StrUtil.isBlank(x_real_ua)) { + this.userAgent = request.getHeader("UserDetail-Agent"); + } else { + this.userAgent = x_real_ua; + } + } + + public static AuthenticationDetails create(HttpServletRequest request) { + return AUTHENTICATION_DETAILS_SOURCE.buildDetails(request); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java new file mode 100644 index 0000000..65839cc --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationConfigurer.java @@ -0,0 +1,83 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.handler.LoginPostHandler; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextRepository; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * 配置器 + */ +public class CombineAuthenticationConfigurer extends AbstractHttpConfigurer { + + /** + * 登陆预处理器 + */ + private final Collection loginPreparers = new ArrayList<>(); + /** + * 认证管理器 + */ + private AuthenticationManager authenticationManager; + /** + * 登陆后置处理器 + */ + private LoginPostHandler loginPostHandler; + + @Override + public void configure(HttpSecurity builder) throws Exception { + SecurityContextRepository securityContextRepository = builder.getSharedObject(SecurityContextRepository.class); + CombineAuthenticationFilter filter = new CombineAuthenticationFilter(loginPreparers, authenticationManager, loginPostHandler, securityContextRepository); + builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); + } + + /** + * 添加登陆预处理器 + * + * @param loginPreparer 登陆预处理器 + * @return 配置器对象 + */ + public CombineAuthenticationConfigurer addLoginPreparer(LoginPreparer loginPreparer) { + loginPreparers.add(loginPreparer); + return this; + } + + /** + * 添加登陆预处理器 + * + * @param loginPreparers 登陆预处理器 + * @return 配置器对象 + */ + public CombineAuthenticationConfigurer loginPreparers(Collection loginPreparers) { + this.loginPreparers.addAll(loginPreparers); + return this; + } + + /** + * 设置认证管理器 + * + * @param authenticationManager 认证管理器 + * @return 配置器对象 + * @see AbstractAuthenticationProvider + */ + public CombineAuthenticationConfigurer authenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + return this; + } + + /** + * 设置登录历史记录器 + * + * @param loginHistoryRecorder 登录历史记录器 + * @return 配置器对象 + * @see DefaultLoginHistoryRecorder + */ + public CombineAuthenticationConfigurer loginHistoryRecorder(LoginHistoryRecorder loginHistoryRecorder) { + this.loginPostHandler = new LoginPostHandler(loginHistoryRecorder); + return this; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java new file mode 100644 index 0000000..fc36105 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/CombineAuthenticationFilter.java @@ -0,0 +1,144 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.handler.LoginPostHandler; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.Collection; +import java.util.Optional; + +/** + * 认证过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class CombineAuthenticationFilter extends GenericFilterBean { + /** + * 支持的登录方式列表 + */ + private final Collection loginPreparers; + + /** + * 认证管理器{@link AbstractAuthenticationProvider} + */ + private final AuthenticationManager authenticationManager; + + /** + * 登陆后置处理器 + */ + private final LoginPostHandler loginPostHandler; + + /** + * TOKEN 存取器 + */ + private final SecurityContextRepository securityContextRepository; + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + Optional loginPreparerOptional = loginPreparers.stream().filter(it -> it.support(request)).findFirst(); + + if (loginPreparerOptional.isEmpty()) { + filterChain.doFilter(request, response); + return; + } + + LoginPreparer loginPreparer = loginPreparerOptional.get(); + + LoginForm loginForm; + try { + loginForm = loginPreparer.createLoginForm(request); + loginPreparer.validate(loginForm); + } catch (AuthenticationException failed) { + unsuccessfulAuthentication(request, response, failed); + return; + } catch (Exception e) { + unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException("登录表单解析失败", e)); + return; + } + + try { + Authentication authentication = createAuthenticationToken(request, loginForm); + // 开始认证 + Authentication authenticationResult = authenticationManager.authenticate(authentication); + // 不为空 则认证成功 + if (authenticationResult != null) { + successfulAuthentication(request, response, authenticationResult); + } + } catch (AuthenticationException failed) { + unsuccessfulAuthentication(request, response, failed); + } + } + + /** + * 构建待认证对象 + * + * @param request HTTP 请求对象 + * @return Authentication 待认证对象 + */ + private Authentication createAuthenticationToken(HttpServletRequest request, LoginForm loginForm) { + // 额外登录信息 + AuthenticationDetails details = AuthenticationDetails.create(request); + + return UserAuthenticationToken.create(loginForm, details); + } + + + /** + * 登录成功 + * + * @param request HTTP 请求对象 + * @param response HTTP 响应对象 + * @param authentication 已认证的认证对象 + */ + private void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + // 保存认证结果 + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + securityContextRepository.saveContext(context, request, response); + + loginPostHandler.recordLoginHistory(authentication); + + loginPostHandler.onAuthenticationSuccess(request, response, authentication); + + } + + /** + * 登录失败 + * + * @param request HTTP 请求对象 + * @param response HTTP 响应对象 + * @param failed 异常对象 + */ + private void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + // 清空认证信息 + SecurityContextHolder.clearContext(); + + // 认证失败后的处理 + loginPostHandler.onAuthenticationFailure(request, response, failed); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java new file mode 100644 index 0000000..439cb97 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultAuthenticationProvider.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.security.support; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +/** + * 认证器 + */ +public class DefaultAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return null; + } + + @Override + public boolean supports(Class authentication) { + return false; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java new file mode 100644 index 0000000..34f768a --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/DefaultLoginHistoryRecorder.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.security.support; + + +import com.njzscloud.common.core.jackson.Jackson; +import lombok.extern.slf4j.Slf4j; + +/** + * 默认登录历史记录器 + */ +@Slf4j +public class DefaultLoginHistoryRecorder implements LoginHistoryRecorder { + @Override + public void record(LoginHistory loginHistory) { + log.info("登陆成功:【{}】", Jackson.toJsonStr(loginHistory)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IAuthService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IAuthService.java new file mode 100644 index 0000000..7a6c162 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/IAuthService.java @@ -0,0 +1,29 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.module.code.CodeLoginForm; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.module.wechat.mini.WechatMiniLoginForm; + +import java.util.Set; + +public interface IAuthService { + default UserDetail selectUser(CodeLoginForm form) { + return null; + } + + default UserDetail selectUser(PasswordLoginForm form) { + return null; + } + + default UserDetail selectUser(WechatMiniLoginForm form) { + return null; + } + + default Set selectRole(Long userId) { + return null; + } + + default UserDetail my(Long userId) { + return null; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java new file mode 100644 index 0000000..3cf049d --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/ITokenService.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.security.support; + +public interface ITokenService { + + void saveToken(UserDetail userDetail); + + UserDetail loadUser(String tokenStr); + + void removeToken(Token token); + + void removeToken(Long userId); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java new file mode 100644 index 0000000..5aecdf9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginForm.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.security.support; + + +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.contant.ClientCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.security.Principal; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Setter +@RequiredArgsConstructor +public abstract class LoginForm implements Constrained, Principal { + /** + * 登录方式 + */ + private final AuthWay authWay; + + private ClientCode clientCode; + + public Tuple2 validate() { + ValidRule[] rules = rules(); + List ruleList = Arrays.stream(rules).collect(Collectors.toList()); + ruleList.add(ValidRule.of(() -> clientCode != null, "未指定要使用的客户端")); + String message = ruleList.stream() + .filter(it -> !it.predicate.get()) + .map(it -> it.message) + .collect(Collectors.joining("\n")); + return Tuple2.of(message.isEmpty(), message); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java new file mode 100644 index 0000000..7d6c414 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistory.java @@ -0,0 +1,66 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.security.contant.AuthWay; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 登录记录 + */ +@Getter +@Setter +@Accessors(chain = true) +public class LoginHistory { + /** + * 用户 Id + */ + private Long userId; + + /** + * 账号 Id + */ + private Long accountId; + + /** + * 登录时间 + */ + private LocalDateTime loginTime; + + /** + * 认证方式 + */ + private AuthWay authWay; + + /** + * 用户代理 + */ + private String userAgent; + + /** + * 用户 IP + */ + private String ip; + + /** + * IP 归属地-省(代码) + */ + private Integer provinceCode; + + /** + * IP 归属地-省(名称) + */ + private String provinceName; + + /** + * IP 归属地-市(代码) + */ + private Integer cityCode; + + /** + * IP 归属地-市(名称) + */ + private String cityName; +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java new file mode 100644 index 0000000..3c5313b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginHistoryRecorder.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.security.support; + + +/** + * 登录历史记录器 + */ +public interface LoginHistoryRecorder { + void record(LoginHistory loginHistory); +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java new file mode 100644 index 0000000..084099b --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/LoginPreparer.java @@ -0,0 +1,35 @@ +package com.njzscloud.common.security.support; + +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.security.ex.UserLoginException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.AuthenticationException; + +/** + * 登录前置处理器 + */ +public interface LoginPreparer { + /** + * 是否处理当前请求 + * + * @param request 请求对象 + * @return true-->处理器可以处理当前请求 + */ + boolean support(HttpServletRequest request); + + /** + * 创建登录表单 + * + * @param request 请求对象 + * @return 登录表单 + */ + LoginForm createLoginForm(HttpServletRequest request); + + default void validate(LoginForm loginForm) throws AuthenticationException { + Tuple2 validate = loginForm.validate(); + if (!validate.get_0()) { + throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, validate.get_1()); + } + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java new file mode 100644 index 0000000..2f86129 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/Token.java @@ -0,0 +1,86 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.IdUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.alibaba.fastjson2.annotation.JSONField; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.security.config.WebSecurityProperties; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.common.security.contant.Constants; + + +/** + * Token + * + * @param userId 用户 Id + * @param accountId 账号 Id + * @param tid TOKEN Id + * @param iat 签发时间(时间戳) + * @param exp 过期时间(时间戳) + * @param authWay 登录方式 + */ +// @Setter +public record Token(long userId, long accountId, String tid, long iat, long exp, @JSONField(serializeUsing = DictObjectSerializer.class, deserializeUsing = DictObjectDeserializer.class) AuthWay authWay) { + /** + * 创建 TOKEN + * + * @param userId 用户 Id + * @param accountId 账号 Id + * @param authWay 登陆方式 + * @return Token 对象 + */ + public static Token of(long userId, long accountId, AuthWay authWay) { + WebSecurityProperties webSecurityProperties = SpringUtil.getBean(WebSecurityProperties.class); + long iat = System.currentTimeMillis(); + String tid = IdUtil.fastSimpleUUID(); + long tokenExp = webSecurityProperties.getTokenExp().getSeconds() * 1000; + long exp = tokenExp > 0 ? iat + tokenExp : 0; + return new Token(userId, accountId, tid, iat, exp, authWay); + } + + public static Token anonymousToken() { + return new Token(0, 0, "0", 0, 0, AuthWay.ANONYMOUS); + } + + /** + * 创建 TOKEN + * + * @param token 字符串形式的 TOKEN + * @return Token 对象 + */ + public static Token of(String token) { + token = Base64.decodeStr(token); + String[] tokenSection = token.split(Constants.TOKEN_STR_SEPARATOR); + long userId = Long.parseLong(tokenSection[0]); + long accountId = Long.parseLong(tokenSection[1]); + String tid = tokenSection[2]; + long iat = Long.parseLong(tokenSection[3]); + long exp = Long.parseLong(tokenSection[4]); + AuthWay authWay = AuthWay.valueOf(tokenSection[5]); + return new Token(userId, accountId, tid, iat, exp, authWay); + } + + /** + * 字符串输出 + *

由 6 段组成,每段逗号分隔,再取 Base64

+ *

userId,accountId,tid,iat,exp,authWay(枚举名称)

+ */ + @Override + public String toString() { + return Base64.encode(userId + Constants.TOKEN_STR_SEPARATOR + + accountId + Constants.TOKEN_STR_SEPARATOR + + tid + Constants.TOKEN_STR_SEPARATOR + + iat + Constants.TOKEN_STR_SEPARATOR + + exp + Constants.TOKEN_STR_SEPARATOR + + authWay); + } + + /** + * 是否过期 + */ + public boolean isExpired() { + return exp > 0 && exp < System.currentTimeMillis() - 10 * 1000; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java new file mode 100644 index 0000000..202a7d6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSecurityContextRepository.java @@ -0,0 +1,124 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.njzscloud.common.security.util.SecurityUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.njzscloud.common.security.contant.Constants.ANONYMOUS_USER; +import static com.njzscloud.common.security.contant.Constants.LOGIN_URL; + +/** + * TOKEN 存取器 + */ +@Slf4j +public class TokenSecurityContextRepository implements SecurityContextRepository { + + /** + * 请求头 TOKEN 匹配正则 + */ + private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^(?[a-zA-Z0-9-.:_~+/]+=*)$", Pattern.CASE_INSENSITIVE); + + /** + * Websocket TOKEN 所在的请求头 + */ + private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + /** + * 解析 TOKEN + * + * @param requestResponseHolder 请求信息存储器 + * @return 认证信息 + */ + @Override + @SuppressWarnings("deprecation") + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + + HttpServletRequest request = requestResponseHolder.getRequest(); + // 普通请求头中的 TOKEN + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + // 从 Query 参数中获取 TOKEN + if (StrUtil.isBlank(authorization)) { + String queryString = request.getQueryString(); + if (StrUtil.isNotBlank(queryString)) { + String[] split = queryString.split("&"); + for (String s : split) { + int idx = s.indexOf('='); + String key = idx > 0 ? URLUtil.decode(s.substring(0, idx), "UTF-8") : s; + if (HttpHeaders.AUTHORIZATION.equalsIgnoreCase(key)) { + authorization = idx > 0 && s.length() > idx + 1 ? URLUtil.decode(s.substring(idx + 1), "UTF-8") : null; + } + } + } + } + + // 从 Websocket 子协议请求头中获取 TOKEN + if (StrUtil.isBlank(authorization)) { + String websocketHeader = request.getHeader(SEC_WEB_SOCKET_PROTOCOL); + if (StrUtil.isNotBlank(websocketHeader)) { + authorization = websocketHeader.split(",")[0].trim(); + } + } + + UserDetail userDetail = null; + + if (StrUtil.isNotBlank(authorization)) { + authorization = URLUtil.decode(authorization); + Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization); + if (matcher.matches()) { + String tokenStr = matcher.group("token"); + try { + userDetail = SecurityUtil.parseToken(tokenStr); + } catch (Exception e) { + log.warn("TOKEN 解析失败", e); + } + } + } + + if (userDetail == null || userDetail.getToken().isExpired()) { + userDetail = ANONYMOUS_USER; + } + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + AuthenticationDetails details = AuthenticationDetails.create(request); + context.setAuthentication(UserAuthenticationToken.create(userDetail, details)); + return context; + } + + /** + * 保存 TOKEN + * + * @param context 认证信息 + * @param request 请求对象 + * @param response 响应对象 + */ + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = context.getAuthentication(); + // 退出登录时会为 null + if (!request.getRequestURI().startsWith(LOGIN_URL) || authentication == null) return; + UserAuthenticationToken userAuthenticationToken = (UserAuthenticationToken) authentication; + UserDetail userDetail = (UserDetail) userAuthenticationToken.getPrincipal(); + SecurityUtil.saveToken(userDetail); + } + + /** + * 始终为 true + */ + @Override + public boolean containsContext(HttpServletRequest request) { + return true; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java new file mode 100644 index 0000000..888c560 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/TokenSerializer.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.security.support; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * TOKEN 序列化器 + */ +public class TokenSerializer extends JsonSerializer { + @Override + public void serialize(Token value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(value.toString()); + } else { + gen.writeNull(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java new file mode 100644 index 0000000..531a5d2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserAuthenticationToken.java @@ -0,0 +1,83 @@ +package com.njzscloud.common.security.support; + +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 待认证/已认证 令牌 + */ +@Getter +public class UserAuthenticationToken extends AbstractAuthenticationToken { + + /** + * 凭证 + */ + private Object credentials; + + /** + * 认证主体,未认证时为:{@link LoginForm},认证完成时为:{@link UserDetail} + */ + private Object principal; + + /** + * 创建认证对象 + * + * @param credentials 凭证 + * @param principal 认证主体,未认证时为:{@link LoginForm},认证完成时为:{@link UserDetail} + * @param authorities 权限信息(角色编码) + * @param details 登录附加信息{@link AuthenticationDetails} + * @param authenticated 是否已完成认证 + */ + private UserAuthenticationToken(Object credentials, + Object principal, + Collection authorities, + AuthenticationDetails details, + boolean authenticated) { + super(authorities); + setAuthenticated(authenticated); + setDetails(details); + this.credentials = credentials; + this.principal = principal; + } + + /** + * 创建未认证的认证对象 + * + * @param principal 认证主体,未认证时为:{@link LoginForm} + * @param details 登录附加信息{@link AuthenticationDetails} + * @return 待认证对象 + */ + public static UserAuthenticationToken create(LoginForm principal, AuthenticationDetails details) { + return new UserAuthenticationToken(null, principal, null, details, false); + } + + /** + * 创建已完成认证的认证对象 + * + * @param principal 用户信息{@link UserDetail} + * @return 已完成认证的认证对象 + */ + public static UserAuthenticationToken create(UserDetail principal, AuthenticationDetails details) { + Token credentials = principal.getToken(); + Set roles = principal.getRoles(); + Set authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()); + return new UserAuthenticationToken(credentials, principal, authorities, details, true); + } + + public void authenticated(UserDetail principal) { + Token credentials = principal.getToken(); + Set roles = principal.getRoles(); + this.credentials = credentials; + this.principal = principal; + setAuthenticated(true); + + // Set authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()); + // return new UserAuthenticationToken(credentials, principal, authorities, details, true); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java new file mode 100644 index 0000000..a3444d4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/UserDetail.java @@ -0,0 +1,101 @@ +package com.njzscloud.common.security.support; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson2.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.njzscloud.common.core.fastjson.serializer.DictObjectDeserializer; +import com.njzscloud.common.core.fastjson.serializer.DictObjectSerializer; +import com.njzscloud.common.security.contant.AuthWay; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.security.core.CredentialsContainer; + +import java.security.Principal; +import java.util.Set; + +import static com.njzscloud.common.security.contant.Constants.ROLE_ANONYMOUS; + +/** + * 用户信息 + */ +@Getter +@Setter +@Accessors(chain = true) +public class UserDetail implements CredentialsContainer, Principal { + + /** + * 用户 Id + */ + private Long userId; + /** + * 昵称 + */ + private String nickname; + /** + * 密码 + */ + @JsonIgnore + private String secret; + /** + * 账号 Id + */ + private Long accountId; + private Long tenantId; + private Integer clientCode; + private String tenantName; + /** + * 登录方式 + */ + @JSONField(serializeUsing = DictObjectSerializer.class, deserializeUsing = DictObjectDeserializer.class) + private AuthWay authWay; + /** + * 角色编码 + */ + private Set roles; + @JsonSerialize(using = TokenSerializer.class) + private Token token; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + public static UserDetail anonymousUser() { + return new UserDetail() + .setUserId(0L) + .setAccountId(0L) + .setRoles(CollUtil.newHashSet(ROLE_ANONYMOUS)) + .setToken(Token.anonymousToken()); + } + + /** + * 账号是否过期 + */ + // private boolean accountExpired = false; + + /** + * 账号是否被锁定 + */ + // private boolean accountLocked = false; + + /** + * 密码是否过期 + */ + // private boolean credentialsExpired = false; + + /** + * 是否启用 + */ + // private boolean disable = false; + @Override + @JsonIgnore + public String getName() { + return userId.toString(); + } + + @Override + public void eraseCredentials() { + this.secret = null; + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/VerificationCodeService.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/VerificationCodeService.java new file mode 100644 index 0000000..78b977e --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/VerificationCodeService.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.security.support; + + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import com.njzscloud.common.core.ex.Exceptions; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class VerificationCodeService { + private final boolean fixed; + + private final TimedCache codeMap = CacheUtil.newTimedCache(1000 * 60 * 5); + private final TimedCache phoneMap = CacheUtil.newTimedCache(1000 * 60); + + public String obtainCode(String phone) { + if (!fixed) { + String s = phoneMap.get(phone); + if (s != null) { + throw Exceptions.clierr("请勿平繁获取"); + } + } + String codeId; + if (fixed) { + codeId = "00000"; + } else { + codeId = IdUtil.simpleUUID(); + } + phoneMap.put(phone, codeId); + String code = null; + if (fixed) { + code = "00000"; + } else { + RandomUtil.randomString(5); + } + + log.info("{} 验证码为:{}", phone, code); + sendCode(phone, code); + codeMap.put(codeId, code); + return codeId; + } + + public boolean checkCode(String codeId, String code) { + if (fixed) { + return true; + } + String oldCode = codeMap.get(codeId); + boolean succ = oldCode != null && oldCode.equals(code); + if (succ) { + codeMap.remove(codeId); + } + return succ; + } + + public void sendCode(String phone, String code) { + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/controller/VerificationCodeController.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/controller/VerificationCodeController.java new file mode 100644 index 0000000..1f0314a --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/support/controller/VerificationCodeController.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.security.support.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.support.VerificationCodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录 + */ +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class VerificationCodeController { + private final VerificationCodeService verificationCodeService; + + /** + * 获取验证码 + * + * @param phone 手机号 + * @return 验证码标识符 + */ + @GetMapping("/obtain_code") + public R obtainCode(@RequestParam("phone") String phone) { + return R.success(verificationCodeService.obtainCode(phone)); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java new file mode 100644 index 0000000..d7e40d7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/EncryptUtil.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.security.util; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.support.VerificationCodeService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * 加密工具 + */ +public class EncryptUtil { + /** + * 加密器 + */ + private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder(); + + /** + * 加密 + * + * @param password 密码(明文) + * @return 密码(密文) + */ + public static String encrypt(String password) { + if (StrUtil.isBlank(password)) return null; + return ENCODER.encode(password); + } + + /** + * 匹配密码 + * + * @param rawPassword 密码(明文) + * @param encodedPassword 密码(密文) + * @return 匹配结果, true: 成功 + */ + public static boolean matches(String rawPassword, String encodedPassword) { + return ENCODER.matches(rawPassword, encodedPassword); + } + + /** + * 获取随机密码, 默认长度为 6 + * + * @return String[ ] 0-->密码(原文), 1-->密码(密文) + */ + public static String[] randomPassword() { + return randomPassword(6); + } + + /** + * 获取随机密码 + * + * @param len 密码长度 + * @return String[ ] 0-->密码(原文), 1-->密码(密文) + */ + public static String[] randomPassword(int len) { + String password = RandomUtil.randomString(Constants.RANDOM_BASE_STRING, len); + String encryptedPassword = encrypt(password); + return new String[]{password, encryptedPassword}; + } + + public static boolean checkCode(String codeId, String code) { + VerificationCodeService verificationCodeService = SpringUtil.getBean(VerificationCodeService.class); + return verificationCodeService.checkCode(codeId, code); + } + + + public static void main(String[] args) { + System.out.println(encrypt("admin")); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java new file mode 100644 index 0000000..08b3ee2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/java/com/njzscloud/common/security/util/SecurityUtil.java @@ -0,0 +1,89 @@ +package com.njzscloud.common.security.util; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.support.ITokenService; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * 获取认证信息工具 + */ +public class SecurityUtil { + /** + * 获取当前登录主体信息 + * + * @return UserAuthPrincipal + */ + @SuppressWarnings("unchecked") + public static T loginUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + T userDetail = null; + if (authentication != null) userDetail = (T) authentication.getPrincipal(); + return userDetail == null ? (T) Constants.ANONYMOUS_USER : userDetail; + } + + /** + * 是否是管理员 + * + * @return true/false + */ + public static boolean isAdmin() { + UserDetail userDetail = SecurityUtil.loginUser(); + return userDetail.getRoles().contains(Constants.ROLE_ADMIN); + } + + /** + * 获取当前登录用户 ID + * + * @return 用户 ID + */ + public static Long currentUserId() { + UserDetail userDetail = SecurityUtil.loginUser(); + return userDetail.getUserId(); + } + + /** + * 保存用户信息和 TOKEN + * + * @param userDetail 用户信息 + */ + public static void saveToken(UserDetail userDetail) { + SpringUtil.getBean(ITokenService.class).saveToken(userDetail); + } + + public static UserDetail registrationUser(UserDetail userDetail) { + Token token = Token.of(userDetail.getUserId(), userDetail.getAccountId(), userDetail.getAuthWay()); + userDetail.setToken(token); + saveToken(userDetail); + return userDetail; + } + + /** + * 解析 TOKEN + * + * @param tokenStr TOKEN 字符串 + * @return Token + * @see Token + */ + public static UserDetail parseToken(String tokenStr) { + return SpringUtil.getBean(ITokenService.class).loadUser(tokenStr); + } + + /** + * 解析 TOKEN + * + * @param token TOKEN + * @see Token + */ + public static void removeToken(Token token) { + SpringUtil.getBean(ITokenService.class).removeToken(token); + } + + + public static void removeToken(Long userId) { + SpringUtil.getBean(ITokenService.class).removeToken(userId); + } +} diff --git a/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..cee43f7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.security.config.WebSecurityAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-sichen/pom.xml b/njzscloud-common/njzscloud-common-sichen/pom.xml new file mode 100644 index 0000000..ae96c08 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-sichen + jar + + sichen + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-mp + provided + + + com.njzscloud + njzscloud-common-mvc + provided + + + diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java new file mode 100644 index 0000000..b5083ea --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskAutoConfiguration.java @@ -0,0 +1,167 @@ +package com.njzscloud.common.sichen.config; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.thread.ThreadPool; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import com.njzscloud.common.sichen.controller.TaskController; +import com.njzscloud.common.sichen.executor.SichenExecutor; +import com.njzscloud.common.sichen.scheduler.SichenScheduler; +import com.njzscloud.common.sichen.service.*; +import com.njzscloud.common.sichen.support.Task; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskStore; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.concurrent.ThreadPoolExecutor; + +@Slf4j +@Configuration +@MapperScan("com.njzscloud.common.sichen.mapper") +@ConditionalOnBooleanProperty(prefix = "sichen.task", name = "enable") +@EnableConfigurationProperties(TaskProperties.class) +public class TaskAutoConfiguration { + + @Bean("dbTaskService") + @ConditionalOnBooleanProperty(prefix = "sichen.task", name = "enable-database", matchIfMissing = true) + public DbTaskService dbTaskService() { + return new DbTaskService(); + } + + @Bean("memoryTaskStoreService") + public MemoryTaskStoreService memoryTaskStoreService() { + return new MemoryTaskStoreService(); + } + + @Bean("combineStoreService") + public CombineStoreService combineStoreService(MemoryTaskStoreService memoryTaskStoreService, @Autowired(required = false) DbTaskService dbTaskService) { + return new CombineStoreService(memoryTaskStoreService, dbTaskService); + } + + @Bean + public TaskController taskController(CombineStoreService combineStoreService) { + return new TaskController(combineStoreService); + } + + @Bean + public TaskExecuteLogService taskExecuteLogService() { + return new TaskExecuteLogService(); + } + + @Bean(destroyMethod = "stop") + public TaskScheduleRecodeService taskScheduleRecodeService(TaskProperties taskProperties, TaskExecuteLogService taskExecuteLogService) { + return new TaskScheduleRecodeService(taskProperties, taskExecuteLogService); + } + + @Bean(destroyMethod = "stop") + public SichenExecutor sichenExecutor(TaskProperties taskProperties, TaskScheduleRecodeService taskScheduleRecodeService) { + TaskProperties.Executor executor = taskProperties.getExecutor(); + ThreadPoolExecutor threadPool; + if (executor.isVirtual()) { + threadPool = ThreadPool.createVirtualThreadPool(executor.getPoolName(), null); + } else { + threadPool = ThreadPool.createPlatformThreadPool(executor.getPoolName(), + executor.getCorePoolSize(), executor.getMaxPoolSize(), + executor.getKeepAliveSeconds(), executor.getWindowCapacity(), executor.getStandbyCapacity(), null); + } + return new SichenExecutor(threadPool, taskScheduleRecodeService); + } + + @Bean(destroyMethod = "stop") + public SichenScheduler sichenScheduler(CombineStoreService combineStoreService, TaskProperties taskProperties) { + TaskProperties.Scheduler scheduler = taskProperties.getScheduler(); + return new SichenScheduler(combineStoreService, scheduler.getScanPeriod(), scheduler.getCompensation(), scheduler.getPoolName(), scheduler.isVirtual()); + } + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReadyEvent() { + ApplicationContext applicationContext = SpringUtil.getApplicationContext(); + String[] beanNames = applicationContext.getBeanDefinitionNames(); + CombineStoreService combineStoreService = applicationContext.getBean(CombineStoreService.class); + for (String beanName : beanNames) { + Object bean = applicationContext.getBean(beanName); + process(bean, combineStoreService); + } + } + + public void process(Object bean, CombineStoreService combineStoreService) throws BeansException { + Class clazz = bean.getClass(); + Method[] methods; + String className; + boolean proxy = AopUtils.isCglibProxy(bean); + if (proxy) { + Class parentClazz = clazz.getSuperclass(); + methods = parentClazz.getMethods(); + className = parentClazz.getCanonicalName(); + } else { + methods = clazz.getMethods(); + className = clazz.getCanonicalName(); + } + HashSet ids = new HashSet<>(); + for (Method method : methods) { + Task task = method.getAnnotation(Task.class); + if (task != null) { + String methodName = method.getName(); + + if (proxy) { + try { + method = clazz.getMethod(methodName, method.getParameterTypes()); + } catch (NoSuchMethodException e) { + continue; + } + } + + String fn = className + "#" + methodName; + + String name = task.name(); + name = StrUtil.isBlank(name) ? fn : name; + + TaskStore.registerFn(fn, bean, method); + + int period = task.period(); + String cron = task.cron(); + if (StrUtil.isNotBlank(cron) || period > 0) { + String scheduleConf = StrUtil.isNotBlank(cron) ? cron : String.valueOf(period); + ScheduleType scheduleType = StrUtil.isNotBlank(cron) ? ScheduleType.Cron : ScheduleType.Fixed; + String memo = task.memo(); + TaskLogLevel taskLogLevel = task.logLevel(); + long id = task.id(); + if (id <= 0) { + log.warn("任务 Id 应大于 0,现在将使用默认生成的 Id,任务名称:{}", name); + id = IdUtil.getSnowflakeNextId(); + } + if (ids.contains(id)) { + log.warn("任务 Id 重复,已跳过"); + continue; + } else { + ids.add(id); + } + combineStoreService.memoryTaskStoreService.registerTask(new TaskInfo() + .setId(id) + .setTaskName(name) + .setMemo(memo) + .setFn(fn) + .setScheduleType(scheduleType) + .setScheduleConf(scheduleConf) + .setLogLevel(taskLogLevel)); + log.info("已注册任务:任务名:{},Id:{}", name, id); + } + } + } + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java new file mode 100644 index 0000000..67e1387 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TaskProperties.java @@ -0,0 +1,121 @@ +package com.njzscloud.common.sichen.config; + +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@ConfigurationProperties("sichen.task") +public class TaskProperties { + + /** + * 是否启用 + */ + private boolean enable = false; + + /** + * 是否启用数据库任务,默认关闭 + */ + private boolean enableDatabase = true; + /** + * 调度器配置 + */ + private Scheduler scheduler = new Scheduler(); + /** + * 执行器配置 + */ + private Executor executor = new Executor(); + /** + * 日志配置 + */ + private Logger logger = new Logger(); + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Scheduler { + + /** + * 任务扫描频率(秒) + */ + private int scanPeriod = 5; + /** + * 补偿时间(秒),0-->忽略,等待下次调度、N-->N 秒内错过的任务将立即触发一次 + */ + private int compensation = 0; + + /** + * 调度器名称 + */ + private String poolName = "调度器"; + + /** + * 是否使用虚拟线程 + */ + private boolean virtual = true; + } + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Executor { + + /** + * 执行器名称 + */ + private String poolName = "执行器"; + /** + * 是否使用虚拟线程 + */ + private boolean virtual = true; + /** + * 核心线程数(平台线程) + */ + private int corePoolSize = 0; + /** + * 最大线程数(平台线程) + */ + private int maxPoolSize = Integer.MAX_VALUE; + /** + * 线程空闲时间(平台线程) + */ + private long keepAliveSeconds = 60; + /** + * 一级队列(平台线程) + */ + private int windowCapacity = 20; + /** + * 二级队列(平台线程) + */ + private int standbyCapacity = 8192; + } + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class Logger { + /** + * 是否启用日志 + */ + private boolean enable = true; + + /** + * 日志留存时间(天),最小只能为 1 + */ + private int history = 15; + + /** + * 全局日志等级 + */ + private TaskLogLevel logLevel = TaskLogLevel.DEBUG; + + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerAutoConfiguration.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerAutoConfiguration.java new file mode 100644 index 0000000..7dfee9b --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.njzscloud.common.sichen.config; + +import com.njzscloud.common.sichen.util.TimerUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@ConditionalOnBooleanProperty(prefix = "sichen.timer", name = "enable") +@EnableConfigurationProperties(TimerProperties.class) +public class TimerAutoConfiguration implements DisposableBean { + public TimerAutoConfiguration(TimerProperties timerProperties) { + TimerUtil.init(timerProperties.isVirtual(), timerProperties.getPoolName(), timerProperties.getCorePoolSize()); + } + + @Override + public void destroy() throws Exception { + TimerUtil.shutdown(); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerProperties.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerProperties.java new file mode 100644 index 0000000..fe12bda --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/config/TimerProperties.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.sichen.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@ConfigurationProperties("sichen.timer") +public class TimerProperties { + private boolean enable = false; + /** + * 执行器名称 + */ + private String poolName = "定时器"; + /** + * 是否使用虚拟线程 + */ + private boolean virtual = true; + /** + * 核心线程数(平台线程) + */ + private int corePoolSize = 0; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java new file mode 100644 index 0000000..3e9d462 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/ScheduleType.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.sichen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ScheduleType implements DictStr { + Manually("Manually", "手动"), + Fixed("Fixed", "固定周期"), + Cron("Cron", "自定义"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskLogLevel.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskLogLevel.java new file mode 100644 index 0000000..16725ff --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskLogLevel.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.sichen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TaskLogLevel implements DictStr { + DEBUG("DEBUG", "调试"), + INFO("INFO", "普通"), + WARN("WARN", "警告"), + ERROR("ERROR", "错误"), + OFF("OFF", "关闭"), + ; + + private final String val; + + private final String txt; +} + diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java new file mode 100644 index 0000000..f7f690e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/contant/TaskStatus.java @@ -0,0 +1,20 @@ +package com.njzscloud.common.sichen.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TaskStatus implements DictStr { + Waiting("Waiting", "等待调度"), + Pending("Pending", "排队中"), + Running("Running", "运行中"), + Completed("Completed", "已完成"), + Error("Error", "错误"); + + private final String val; + + private final String txt; +} + diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/controller/TaskController.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/controller/TaskController.java new file mode 100644 index 0000000..4ed6cda --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/controller/TaskController.java @@ -0,0 +1,111 @@ +package com.njzscloud.common.sichen.controller; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sichen.pojo.param.AddTaskParam; +import com.njzscloud.common.sichen.pojo.param.ModifyTaskParam; +import com.njzscloud.common.sichen.pojo.param.SearchTaskParam; +import com.njzscloud.common.sichen.pojo.param.SearchTaskResult; +import com.njzscloud.common.sichen.service.CombineStoreService; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskStore; +import com.njzscloud.common.sichen.util.TaskUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 用户定时任务 + */ +@Slf4j +@RestController +@RequestMapping("/task") +@RequiredArgsConstructor +public class TaskController { + + private final CombineStoreService taskStore; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody @Validated AddTaskParam addTaskParam) { + taskStore.registerTask(BeanUtil.copyProperties(addTaskParam, TaskInfo.class)); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(ModifyTaskParam modifyTaskParam) { + taskStore.dbTaskService.modify(modifyTaskParam); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, () -> Exceptions.clierr("未指定要删除的数据")); + taskStore.dbTaskService.del(ids); + return R.success(); + } + + /** + * 处理函数列表 + */ + @GetMapping("/fn") + public R>> fn(String keywords) { + return R.success(TaskStore.searchFn(keywords)); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam("id") Long id) { + TaskStore.RLOCK.lock(); + try { + SearchTaskResult searchTaskResult = taskStore.memoryTaskStoreService.detail(id); + if (searchTaskResult == null) searchTaskResult = taskStore.dbTaskService.detail(id); + return R.success(searchTaskResult); + } finally { + TaskStore.RLOCK.unlock(); + } + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SearchTaskParam searchTaskParam) { + TaskStore.RLOCK.lock(); + try { + if (searchTaskParam.isBuiltin()) { + return R.success(taskStore.memoryTaskStoreService.paging(pageParam, searchTaskParam)); + } else { + return R.success(taskStore.dbTaskService.paging(pageParam, searchTaskParam)); + } + } finally { + TaskStore.RLOCK.unlock(); + } + } + + /** + * 触发一次任务 + */ + @GetMapping("/trigger") + public R trigger(@RequestParam("id") Long id) { + return R.success(TaskUtil.trigger(id)); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java new file mode 100644 index 0000000..48d57de --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/executor/SichenExecutor.java @@ -0,0 +1,52 @@ +package com.njzscloud.common.sichen.executor; + +import com.njzscloud.common.sichen.service.TaskScheduleRecodeService; +import com.njzscloud.common.sichen.support.TaskHandle; +import com.njzscloud.common.sichen.support.TaskInfo; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; + +@Slf4j +public class SichenExecutor { + private final ThreadPoolExecutor taskThreadPool; + private final TaskScheduleRecodeService taskScheduleRecodeService; + + public SichenExecutor(ThreadPoolExecutor taskThreadPool, TaskScheduleRecodeService taskScheduleRecodeService) { + log.info("任务执行器启动中..."); + this.taskScheduleRecodeService = taskScheduleRecodeService; + this.taskThreadPool = taskThreadPool; + log.info("任务执行器已启动"); + } + + public void execute(TaskInfo task, boolean manually) { + if (task == null) { + log.warn("未提交任务"); + return; + } + TaskHandle taskHandle = new TaskHandle(taskScheduleRecodeService, manually, task); + try { + taskHandle.pending(); + taskThreadPool.execute(taskHandle); + } catch (Exception e) { + log.error("任务调度失败,任务:{}", taskHandle.getTaskName(), e); + } + } + + public void execute(List tasks, boolean manually) { + if (tasks == null || tasks.isEmpty()) { + log.warn("任务列表不能为空"); + return; + } + for (TaskInfo task : tasks) { + this.execute(task, manually); + } + } + + public void stop() { + log.info("正在停止任务执行器..."); + taskThreadPool.shutdownNow(); + log.info("任务执行器已停止"); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskExecuteLogMapper.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskExecuteLogMapper.java new file mode 100644 index 0000000..18438cb --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskExecuteLogMapper.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.sichen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskExecuteLogEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TaskExecuteLogMapper extends BaseMapper { +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskMapper.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskMapper.java new file mode 100644 index 0000000..c7f27fb --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.common.sichen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 定时任务表 + */ +@Mapper +public interface TaskMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskScheduleRecodeMapper.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskScheduleRecodeMapper.java new file mode 100644 index 0000000..7fff8f5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/mapper/TaskScheduleRecodeMapper.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.sichen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskScheduleRecodeEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TaskScheduleRecodeMapper extends BaseMapper { +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskEntity.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskEntity.java new file mode 100644 index 0000000..afa2860 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskEntity.java @@ -0,0 +1,75 @@ +package com.njzscloud.common.sichen.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_task", autoResultMap = true) +public class TaskEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 任务名称 + */ + private String taskName; + + /** + * 任务执行函数;Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 临界时间 + */ + private Long criticalTiming; + + /** + * 是否禁用; 0-->否、1-->是 + */ + private Boolean disabled; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 备注 + */ + private String memo; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskExecuteLogEntity.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskExecuteLogEntity.java new file mode 100644 index 0000000..436defd --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskExecuteLogEntity.java @@ -0,0 +1,61 @@ +package com.njzscloud.common.sichen.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 任务执行日志表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_task_execute_log", autoResultMap = true) +public class TaskExecuteLogEntity { + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 调度 Id + */ + private Long scheduleId; + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 日志时间 + */ + private LocalDateTime logTime; + /** + * 位置 + */ + private String place; + /** + * 行号 + */ + private Integer line; + /** + * 日志信息 + */ + private String msg; + /** + * 错误信息 + */ + private String err; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskScheduleRecodeEntity.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskScheduleRecodeEntity.java new file mode 100644 index 0000000..442f235 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/entity/TaskScheduleRecodeEntity.java @@ -0,0 +1,87 @@ +package com.njzscloud.common.sichen.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskStatus; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 任务调度记录表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_task_schedule_recode") +public class TaskScheduleRecodeEntity { + /** + * Id + */ + @TableId(type = IdType.INPUT) + private Long id; + /** + * 任务 Id + */ + private Long taskId; + /** + * 任务名称 + */ + private String taskName; + + /** + * 任务执行函数;Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 调度时间 + */ + private LocalDateTime scheduleTime; + /** + * 任务开始时间 + */ + private LocalDateTime startTime; + /** + * 任务结束时间 + */ + private LocalDateTime endTime; + + /** + * 任务状态 + */ + private TaskStatus taskStatus; + /** + * 本次调度是否为手动触发 + */ + private Boolean manually; + + private Boolean builtin; + + /** + * 备注 + */ + private String memo; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/AddTaskParam.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/AddTaskParam.java new file mode 100644 index 0000000..257d56a --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/AddTaskParam.java @@ -0,0 +1,66 @@ +package com.njzscloud.common.sichen.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class AddTaskParam implements Constrained { + + /** + * 任务名称:Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String taskName; + + /** + * 任务执行函数,全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 是否禁用; 0-->否、1-->是 + */ + private Boolean disabled; + /** + * 备注 + */ + private String memo; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(taskName), "任务名称不能为空"), + ValidRule.of(() -> StrUtil.isNotBlank(fn), "任务执行函数不能为空"), + ValidRule.of(() -> disabled != null, "是否禁用不能为空"), + ValidRule.of(() -> scheduleType != null, "调度方式不能为空"), + ValidRule.of(() -> scheduleType == ScheduleType.Manually ? StrUtil.isBlank(scheduleConf) : StrUtil.isNotBlank(scheduleConf), "调度方式或调度配置错误"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/ModifyTaskParam.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/ModifyTaskParam.java new file mode 100644 index 0000000..10e989f --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/ModifyTaskParam.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.sichen.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class ModifyTaskParam implements Constrained { + @NotNull(message = "未指定要修改的数据") + private Long id; + + /** + * 任务名称:Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String taskName; + + /** + * 任务执行函数,全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 是否禁用; 0-->否、1-->是 + */ + private Boolean disabled; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 备注 + */ + private String memo; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(taskName) + || StrUtil.isNotBlank(fn) + || scheduleType != null + || StrUtil.isNotBlank(scheduleConf) + || disabled != null + || StrUtil.isNotBlank(memo), + "未指定要修改的内容"), + ValidRule.of(() -> scheduleType == null || (scheduleType == ScheduleType.Manually ? StrUtil.isBlank(scheduleConf) : StrUtil.isNotBlank(scheduleConf)), "调度方式或调度配置错误"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskParam.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskParam.java new file mode 100644 index 0000000..8ff7951 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskParam.java @@ -0,0 +1,40 @@ +package com.njzscloud.common.sichen.pojo.param; + +import com.njzscloud.common.sichen.contant.ScheduleType; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class SearchTaskParam { + + /** + * 任务名称:Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String taskName; + + /** + * 任务执行函数,全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 是否禁用; 0-->否、1-->是 + */ + private Boolean disabled; + + private boolean builtin; + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskResult.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskResult.java new file mode 100644 index 0000000..3af9b09 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/pojo/param/SearchTaskResult.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.sichen.pojo.param; + +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 定时任务表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class SearchTaskResult { + private Long id; + + /** + * 任务名称:Task 注解的 value 属性,或执行函数的全限定名称 + */ + private String taskName; + + /** + * 任务执行函数,全限定名称 + */ + private String fn; + + /** + * 调度方式; 字典代码:schedule_type + */ + private ScheduleType scheduleType; + + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private String scheduleConf; + + /** + * 是否禁用; 0-->否、1-->是 + */ + private boolean disabled; + /** + * 是否为内内置任务 + */ + private boolean builtin; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 下次执行时间 + */ + private long criticalTiming; + /** + * 备注 + */ + private String memo; + + public LocalDateTime getNextTiming() { + return criticalTiming <= 0 ? null : Instant.ofEpochSecond(criticalTiming) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/scheduler/SichenScheduler.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/scheduler/SichenScheduler.java new file mode 100644 index 0000000..d613ecd --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/scheduler/SichenScheduler.java @@ -0,0 +1,182 @@ +package com.njzscloud.common.sichen.scheduler; + +import cn.hutool.core.thread.ThreadUtil; +import com.njzscloud.common.sichen.service.CombineStoreService; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.util.TaskUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class SichenScheduler { + private final Map> todolist = new ConcurrentHashMap<>(); + private final CombineStoreService taskStore; + private final Thread scanThread; + private final Thread dripThread; + + /** + * 任务扫描频率(毫秒) + */ + private final int scanPeriod; + /** + * 补偿时间(秒),0-->忽略,等待下次调度、N-->N 秒内错过的任务将立即触发一次 + */ + private final int compensation; + + public SichenScheduler(CombineStoreService taskStore, int scanPeriod, int compensation, String poolName, boolean virtual) { + log.info("任务调度器启动中..."); + this.taskStore = taskStore; + this.scanPeriod = scanPeriod * 1000; + this.compensation = compensation; + scanThread = createWorkThread(virtual, poolName + "-任务扫描", this::scanTask); + dripThread = createWorkThread(virtual, poolName + "-时间轮", this::drip); + log.info("任务调度器已启动"); + } + + private Thread createWorkThread(boolean virtual, String threadName, Runnable task) { + return virtual ? Thread.ofVirtual() + .name(threadName) + .start(task) + : Thread.ofPlatform() + .name(threadName) + .daemon(true) + .start(task); + } + + public void stop() { + log.info("正在停止任务调度器..."); + ThreadUtil.interrupt(scanThread, true); + ThreadUtil.interrupt(dripThread, true); + log.info("任务调度器已停止"); + } + + /** + * 任务扫描 + */ + private void scanTask() { + long cost = 0; + while (true) { + long timeout = cost == 0 ? + (1000 - System.currentTimeMillis() % 1000) * 5 : + scanPeriod - cost; + if (timeout > 0) { + try { + TimeUnit.MILLISECONDS.sleep(timeout); + } catch (InterruptedException e) { + log.info("任务扫描线程被中断"); + return; + } + } + + long now = System.currentTimeMillis(); + long soon = now + scanPeriod; + + try { + // 从数据库或内存读取任务 + long soon_s = soon / 1000; + List taskInfos = taskStore.loadTask(soon_s); + if (taskInfos != null && !taskInfos.isEmpty()) { + schedule(now / 1000, soon_s, taskInfos); + } + } catch (Exception e) { + log.error("任务调度失败", e); + } + + cost = System.currentTimeMillis() - now; + } + } + + /** + * 任务调度 + * + * @param now 当前时间(秒) + * @param soon 未来时间(秒) + * @param taskInfos 任务列表 + */ + private void schedule(long now, long soon, List taskInfos) { + try { + List updateList = new LinkedList<>(); + for (TaskInfo taskInfo : taskInfos) { + try { + long lastCriticalTiming = taskInfo.getCriticalTiming(); + long delta = lastCriticalTiming - now; + if (delta == 0) { + // 已到时间,立即执行 + TaskUtil.execute(taskInfo); + } else if (delta > 0) { + // 快到时间 + todolist.compute(lastCriticalTiming % 60, (k, v) -> { + if (v == null) v = new LinkedList<>(); + v.add(taskInfo); + return v; + }); + } else { + // 错过的任务,仅执行相差 5 秒的任务,其他忽略等待下次调度 + if (compensation > 0 && -delta <= compensation) TaskUtil.execute(taskInfo); + // 修正调度时间 + lastCriticalTiming = now; + } + long nextCriticalTiming = lastCriticalTiming; + for (int i = 0; i < 5; i++) { + // 下次执行时间 + lastCriticalTiming = TaskUtil.computedNextTiming(lastCriticalTiming, taskInfo); + // 计算失败或刚调度过或无需执行的任务,忽略 + if (lastCriticalTiming == 0 || nextCriticalTiming == lastCriticalTiming) continue; + // 即将执行 + if (lastCriticalTiming > now && lastCriticalTiming <= soon) { + todolist.compute(lastCriticalTiming % 60, (k, v) -> { + if (v == null) v = new LinkedList<>(); + v.add(taskInfo); + return v; + }); + nextCriticalTiming = lastCriticalTiming; + } else if (lastCriticalTiming > soon) { + // 未来执行 + nextCriticalTiming = lastCriticalTiming; + break; + } else { + log.warn("下次执行时间计算失败,执行时间小于当前时间,任务:{},当前时间:{}", taskInfo, now); + nextCriticalTiming = now; + } + } + // 更新下次触发时间 + updateList.add(new TaskInfo(taskInfo).setCriticalTiming(nextCriticalTiming)); + } catch (Exception e) { + log.error("任务调度失败", e); + } + } + for (TaskInfo taskInfo : updateList) { + taskStore.updateCriticalTiming(taskInfo.isBuiltin(), taskInfo.getId(), taskInfo.getCriticalTiming()); + } + } catch (Exception e) { + log.error("任务调度失败", e); + } + } + + /** + * 时间轮 + */ + private void drip() { + while (true) { + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + log.info("计时器线程被中断"); + return; + } + + long now = System.currentTimeMillis() / 1000; + todolist.compute(now % 60, (k, v) -> { + if (v == null || v.isEmpty()) return v; + TaskUtil.execute(v); + v.clear(); + return v; + }); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/CombineStoreService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/CombineStoreService.java new file mode 100644 index 0000000..939e9b3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/CombineStoreService.java @@ -0,0 +1,72 @@ +package com.njzscloud.common.sichen.service; + +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskStore; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class CombineStoreService implements TaskStore { + public final MemoryTaskStoreService memoryTaskStoreService; + public final DbTaskService dbTaskService; + + @Override + public List loadTask(long soon) { + RLOCK.lock(); + try { + List memTaskInfos = memoryTaskStoreService.loadTask(soon); + if (dbTaskService != null) { + List dbTaskInfos = dbTaskService.loadTask(soon); + memTaskInfos.addAll(dbTaskInfos); + } + return memTaskInfos; + } finally { + RLOCK.unlock(); + } + } + + @Override + public boolean trigger(Long id) { + RLOCK.lock(); + try { + boolean trigger = memoryTaskStoreService.trigger(id); + if (!trigger && dbTaskService != null) { + return dbTaskService.trigger(id); + } + return trigger; + } finally { + RLOCK.unlock(); + } + } + + @Override + public void registerTask(TaskInfo taskInfo) { + WLOCK.lock(); + try { + if (dbTaskService != null) dbTaskService.registerTask(taskInfo); + else memoryTaskStoreService.registerTask(taskInfo); + } finally { + WLOCK.unlock(); + } + } + + public void updateCriticalTiming(boolean builtin, long id, long criticalTiming) { + WLOCK.lock(); + try { + if (builtin) memoryTaskStoreService.updateCriticalTiming(id, criticalTiming); + else if (dbTaskService != null) { + dbTaskService.updateCriticalTiming(id, criticalTiming); + } + } finally { + WLOCK.unlock(); + } + } + + @Override + public void updateCriticalTiming(long id, long criticalTiming) { + throw Exceptions.exception("勿用"); + } + +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/DbTaskService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/DbTaskService.java new file mode 100644 index 0000000..c16fe00 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/DbTaskService.java @@ -0,0 +1,155 @@ +package com.njzscloud.common.sichen.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import com.njzscloud.common.sichen.mapper.TaskMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskEntity; +import com.njzscloud.common.sichen.pojo.param.ModifyTaskParam; +import com.njzscloud.common.sichen.pojo.param.SearchTaskParam; +import com.njzscloud.common.sichen.pojo.param.SearchTaskResult; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskStore; +import com.njzscloud.common.sichen.util.TaskUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 定时任务表 + */ +@Slf4j +public class DbTaskService extends ServiceImpl implements TaskStore, IService { + + /** + * 修改 + */ + public void modify(ModifyTaskParam modifyTaskParam) { + WLOCK.lock(); + try { + TaskEntity oldData = this.getById(modifyTaskParam.getId()); + Assert.notNull(oldData, () -> Exceptions.clierr("任务不存在,无法修改")); + ScheduleType scheduleType = modifyTaskParam.getScheduleType(); + String scheduleConf = modifyTaskParam.getScheduleConf(); + + Long criticalTiming = null; + if (scheduleType != null && StrUtil.isNotBlank(scheduleConf)) { + if (scheduleType != oldData.getScheduleType() || scheduleConf.equals(oldData.getScheduleConf())) { + criticalTiming = TaskUtil.computedNextTiming(scheduleType, scheduleConf); + Boolean disabled = modifyTaskParam.getDisabled(); + disabled = disabled == null ? oldData.getDisabled() : disabled; + modifyTaskParam.setDisabled(scheduleType != ScheduleType.Manually && (criticalTiming <= 0 || disabled)); + } + } else { + modifyTaskParam.setScheduleType(null); + modifyTaskParam.setScheduleConf(null); + } + + TaskEntity taskEntity = BeanUtil.copyProperties(modifyTaskParam, TaskEntity.class) + .setCriticalTiming(criticalTiming); + this.updateById(taskEntity); + } finally { + WLOCK.unlock(); + } + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + WLOCK.lock(); + try { + this.removeBatchByIds(ids); + } finally { + WLOCK.unlock(); + } + } + + /** + * 详情 + */ + public SearchTaskResult detail(Long id) { + TaskEntity taskEntity = this.getById(id); + if (taskEntity == null) return null; + return BeanUtil.copyProperties(taskEntity, SearchTaskResult.class) + .setBuiltin(Boolean.FALSE); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, SearchTaskParam searchTaskParam) { + String taskName = searchTaskParam.getTaskName(); + String fn = searchTaskParam.getFn(); + ScheduleType scheduleType = searchTaskParam.getScheduleType(); + Boolean disabled = searchTaskParam.getDisabled(); + return PageResult.of(this.page(pageParam.toPage(), Wrappers.lambdaQuery() + .like(StrUtil.isNotBlank(taskName), TaskEntity::getTaskName, taskName) + .like(StrUtil.isNotBlank(fn), TaskEntity::getFn, fn) + .eq(scheduleType != null, TaskEntity::getScheduleType, scheduleType) + .eq(disabled != null, TaskEntity::getDisabled, disabled) + ).convert(it -> BeanUtil.copyProperties(it, SearchTaskResult.class) + .setBuiltin(Boolean.FALSE) + )); + } + + @Override + public void registerTask(TaskInfo taskInfo) { + ScheduleType scheduleType = taskInfo.getScheduleType(); + long criticalTiming = TaskUtil.computedNextTiming(scheduleType, taskInfo.getScheduleConf()); + boolean disabled = scheduleType != ScheduleType.Manually && (criticalTiming <= 0 || taskInfo.isDisabled()); + TaskEntity taskEntity = BeanUtil.copyProperties(taskInfo, TaskEntity.class) + .setCriticalTiming(criticalTiming) + .setDisabled(disabled); + TaskLogLevel logLevel = taskInfo.getLogLevel(); + if (logLevel == null) { + taskEntity.setLogLevel(TaskLogLevel.INFO); + } + + this.save(taskEntity); + } + + @Override + public List loadTask(long soon) { + return this.list(Wrappers.lambdaQuery() + .gt(TaskEntity::getCriticalTiming, 0) + .le(TaskEntity::getCriticalTiming, soon) + .eq(TaskEntity::getDisabled, false) + .ne(TaskEntity::getScheduleType, ScheduleType.Manually)) + .stream() + .map(it -> BeanUtil.copyProperties(it, TaskInfo.class).setBuiltin(false)) + .toList(); + } + + @Override + public void updateCriticalTiming(long id, long criticalTiming) { + boolean exists = this.exists(Wrappers.lambdaQuery().eq(TaskEntity::getId, id)); + if (!exists) return; + this.updateById(new TaskEntity() + .setId(id) + .setCriticalTiming(criticalTiming) + .setDisabled(criticalTiming <= 0)); + + } + + @Override + public boolean trigger(Long id) { + TaskEntity taskEntity = this.getById(id); + if (taskEntity != null) { + TaskUtil.execute(BeanUtil.copyProperties(taskEntity, TaskInfo.class), true); + return true; + } else { + return false; + } + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/MemoryTaskStoreService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/MemoryTaskStoreService.java new file mode 100644 index 0000000..d329902 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/MemoryTaskStoreService.java @@ -0,0 +1,98 @@ +package com.njzscloud.common.sichen.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.pojo.param.SearchTaskParam; +import com.njzscloud.common.sichen.pojo.param.SearchTaskResult; +import com.njzscloud.common.sichen.support.TaskInfo; +import com.njzscloud.common.sichen.support.TaskStore; +import com.njzscloud.common.sichen.util.TaskUtil; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class MemoryTaskStoreService implements TaskStore { + List TASKS = new LinkedList<>(); + + @Override + public void registerTask(TaskInfo taskInfo) { + ScheduleType scheduleType = taskInfo.getScheduleType(); + long criticalTiming = TaskUtil.computedNextTiming(scheduleType, taskInfo.getScheduleConf()); + boolean disabled = scheduleType != ScheduleType.Manually && (criticalTiming <= 0 || taskInfo.isDisabled()); + TASKS.add(new TaskInfo(taskInfo) + .setCriticalTiming(criticalTiming) + .setDisabled(disabled) + .setBuiltin(true) + ); + } + + @Override + public List loadTask(long soon) { + return TASKS.stream() + .filter(it -> { + long criticalTiming = it.getCriticalTiming(); + ScheduleType scheduleType = it.getScheduleType(); + boolean disabled = it.isDisabled(); + return !disabled && (criticalTiming > 0 && criticalTiming <= soon) && scheduleType != ScheduleType.Manually; + }).map(TaskInfo::new).collect(Collectors.toList()); + } + + + @Override + public void updateCriticalTiming(long id, long criticalTiming) { + TASKS.stream().filter(it -> it.getId() == id) + .findFirst() + .ifPresent(it -> it.setCriticalTiming(criticalTiming) + .setDisabled(criticalTiming <= 0)); + } + + @Override + public boolean trigger(Long id) { + Optional optional = TASKS.stream().filter(it -> it.getId() == id).findFirst(); + if (optional.isPresent()) { + TaskUtil.execute(optional.get(), true); + return true; + } else { + return false; + } + } + + public PageResult paging(PageParam pageParam, SearchTaskParam searchTaskParam) { + Integer current = pageParam.getCurrent(); + Integer size = pageParam.getSize(); + String taskName = searchTaskParam.getTaskName(); + String fn = searchTaskParam.getFn(); + ScheduleType scheduleType = searchTaskParam.getScheduleType(); + + return PageResult.of(current, size, + TASKS.stream() + .filter(it -> { + boolean f = true; + if (StrUtil.isNotBlank(taskName)) { + f = it.getTaskName().contains(taskName); + } + if (StrUtil.isNotBlank(fn)) { + f = f && it.getFn().contains(fn); + } + if (scheduleType != null) { + f = f && (scheduleType == it.getScheduleType()); + } + return f; + }) + .map(it -> BeanUtil.copyProperties(it, SearchTaskResult.class) + .setBuiltin(Boolean.TRUE) + ).toList() + ); + } + + public SearchTaskResult detail(Long id) { + Optional optional = TASKS.stream().filter(it -> it.getId() == id).findFirst(); + return optional.map(it -> BeanUtil.copyProperties(it, SearchTaskResult.class) + .setBuiltin(Boolean.TRUE)).orElse(null); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskExecuteLogService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskExecuteLogService.java new file mode 100644 index 0000000..4cb4948 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskExecuteLogService.java @@ -0,0 +1,9 @@ +package com.njzscloud.common.sichen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.sichen.mapper.TaskExecuteLogMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskExecuteLogEntity; + +public class TaskExecuteLogService extends ServiceImpl implements IService { +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskScheduleRecodeService.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskScheduleRecodeService.java new file mode 100644 index 0000000..4a14ccf --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/service/TaskScheduleRecodeService.java @@ -0,0 +1,138 @@ +package com.njzscloud.common.sichen.service; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.CallerUtil; +import com.njzscloud.common.sichen.config.TaskProperties; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import com.njzscloud.common.sichen.mapper.TaskScheduleRecodeMapper; +import com.njzscloud.common.sichen.pojo.entity.TaskExecuteLogEntity; +import com.njzscloud.common.sichen.pojo.entity.TaskScheduleRecodeEntity; +import com.njzscloud.common.sichen.support.Task; +import com.njzscloud.common.sichen.support.TaskHandle; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.concurrent.LinkedBlockingQueue; + +@Slf4j +public class TaskScheduleRecodeService extends ServiceImpl implements IService { + public final LinkedBlockingQueue logQueue; + private final TaskExecuteLogService taskExecuteLogService; + private final TaskProperties taskProperties; + private final Thread recodeLogThread; + + public TaskScheduleRecodeService(TaskProperties taskProperties, TaskExecuteLogService taskExecuteLogService) { + this.taskProperties = taskProperties; + this.taskExecuteLogService = taskExecuteLogService; + logQueue = new LinkedBlockingQueue<>(Integer.MAX_VALUE); + recodeLogThread = Thread.ofVirtual() + .name("任务日志记录") + .start(this::recodeLog); + TaskProperties.Logger logger = taskProperties.getLogger(); + if (!logger.isEnable()) { + log.info("任务日志未启用"); + } else { + log.info("全局任务日志等级为:{}", logger.getLogLevel()); + } + } + + private void recodeLog() { + while (true) { + try { + logQueue.take().run(); + } catch (InterruptedException e) { + log.info("线程中断,日志记录停止"); + break; + } catch (Exception e) { + log.error("日志记录失败", e); + } + } + } + + public void stop() { + ThreadUtil.interrupt(recodeLogThread, true); + } + + public void schedule(TaskHandle taskHandle) { + logQueue.offer(() -> this.save(new TaskScheduleRecodeEntity() + .setId(taskHandle.getScheduleId()) + .setTaskId(taskHandle.getTaskId()) + .setTaskName(taskHandle.getTaskName()) + .setMemo(taskHandle.getMemo()) + .setFn(taskHandle.getFn()) + .setScheduleType(taskHandle.getScheduleType()) + .setScheduleConf(taskHandle.getScheduleConf()) + .setScheduleTime(taskHandle.getScheduleTime()) + .setTaskStatus(taskHandle.getTaskStatus()) + .setBuiltin(taskHandle.isBuiltin()) + .setManually(taskHandle.isManually()))); + } + + public void pending(TaskHandle taskHandle) { + logQueue.offer(() -> this.updateById(new TaskScheduleRecodeEntity() + .setId(taskHandle.getScheduleId()) + .setTaskStatus(taskHandle.getTaskStatus()))); + } + + public void start(TaskHandle taskHandle) { + logQueue.offer(() -> this.updateById(new TaskScheduleRecodeEntity() + .setId(taskHandle.getScheduleId()) + .setStartTime(taskHandle.getStartTime()) + .setTaskStatus(taskHandle.getTaskStatus()))); + } + + public void end(TaskHandle taskHandle) { + logQueue.offer(() -> this.updateById(new TaskScheduleRecodeEntity() + .setId(taskHandle.getScheduleId()) + .setEndTime(taskHandle.getEndTime()) + .setTaskStatus(taskHandle.getTaskStatus()))); + } + + public void log(TaskHandle taskHandle, TaskLogLevel logLevel, Throwable throwable, String msg, Object... params) { + TaskProperties.Logger logger = taskProperties.getLogger(); + if (!logger.isEnable()) return; + TaskLogLevel globalLogLevel = logger.getLogLevel(); + if (logLevel.ordinal() < globalLogLevel.ordinal()) return; + LocalDateTime logTime = LocalDateTime.now(); + + logQueue.offer(() -> { + Integer lineNumber = null; + String place = ""; + String err = ""; + if (throwable != null) { + Tuple2 tuple2 = CallerUtil.printStackTrace(throwable); + err = tuple2.get_0(); + CallerUtil.Caller caller = tuple2.get_1(); + String className = caller.className(); + String methodName = caller.methodName(); + lineNumber = caller.lineNumber(); + place = className + "#" + methodName; + + } + taskExecuteLogService.save(new TaskExecuteLogEntity() + .setScheduleId(taskHandle.getScheduleId()) + .setLogLevel(logLevel) + .setMsg(StrUtil.format(msg, params)) + .setPlace(place) + .setLine(lineNumber) + .setErr(err) + .setLogTime(logTime)); + }); + } + + @Task(id = 1, name = "日志清理", logLevel = TaskLogLevel.INFO, cron = "1 0 0 * * ?") + public void clearLog() { + TaskProperties.Logger logger = taskProperties.getLogger(); + int history = logger.getHistory(); + if (history < 1) history = 1; + DateTime historyTime = DateUtil.beginOfDay(DateUtil.offsetDay(new DateTime(), -history)); + taskExecuteLogService.remove(Wrappers.lambdaQuery(TaskExecuteLogEntity.class).lt(TaskExecuteLogEntity::getLogTime, historyTime)); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java new file mode 100644 index 0000000..e51be99 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/CronExpression.java @@ -0,0 +1,1662 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package com.njzscloud.common.sichen.support; + +import java.text.ParseException; +import java.util.*; + +/** + * Provides a parser and evaluator for unix-like cron expressions. Cron + * expressions provide the ability to specify complex time combinations such as + * "At 8:00am every Monday through Friday" or "At 1:30am every + * last Friday of the month". + *

+ * Cron expressions are comprised of 6 required fields and one optional field + * separated by white space. The fields respectively are described as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Field Name Allowed Values Allowed Special Characters
Seconds  + * 0-59  + * , - * /
Minutes  + * 0-59  + * , - * /
Hours  + * 0-23  + * , - * /
Day-of-month  + * 1-31  + * , - * ? / L W
Month  + * 0-11 or JAN-DEC  + * , - * /
Day-of-Week  + * 1-7 or SUN-SAT  + * , - * ? / L #
Year (Optional)  + * empty, 1970-2199  + * , - * /
+ *

+ * The '*' character is used to specify all values. For example, "*" + * in the minute field means "every minute". + *

+ * The '?' character is allowed for the day-of-month and day-of-week fields. It + * is used to specify 'no specific value'. This is useful when you need to + * specify something in one of the two fields, but not the other. + *

+ * The '-' character is used to specify ranges For example "10-12" in + * the hour field means "the hours 10, 11 and 12". + *

+ * The ',' character is used to specify additional values. For example + * "MON,WED,FRI" in the day-of-week field means "the days Monday, + * Wednesday, and Friday". + *

+ * The '/' character is used to specify increments. For example "0/15" + * in the seconds field means "the seconds 0, 15, 30, and 45". And + * "5/15" in the seconds field means "the seconds 5, 20, 35, and + * 50". Specifying '*' before the '/' is equivalent to specifying 0 is + * the value to start with. Essentially, for each field in the expression, there + * is a set of numbers that can be turned on or off. For seconds and minutes, + * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to + * 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn + * on every "nth" value in the given set. Thus "7/6" in the + * month field only turns on month "7", it does NOT mean every 6th + * month, please note that subtlety. + *

+ * The 'L' character is allowed for the day-of-month and day-of-week fields. + * This character is short-hand for "last", but it has different + * meaning in each of the two fields. For example, the value "L" in + * the day-of-month field means "the last day of the month" - day 31 + * for January, day 28 for February on non-leap years. If used in the + * day-of-week field by itself, it simply means "7" or + * "SAT". But if used in the day-of-week field after another value, it + * means "the last xxx day of the month" - for example "6L" + * means "the last friday of the month". You can also specify an offset + * from the last day of the month, such as "L-3" which would mean the third-to-last + * day of the calendar month. When using the 'L' option, it is important not to + * specify lists, or ranges of values, as you'll get confusing/unexpected results. + *

+ * The 'W' character is allowed for the day-of-month field. This character + * is used to specify the weekday (Monday-Friday) nearest the given day. As an + * example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of + * the month". So if the 15th is a Saturday, the trigger will fire on + * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the + * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. + * However if you specify "1W" as the value for day-of-month, and the + * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not + * 'jump' over the boundary of a month's days. The 'W' character can only be + * specified when the day-of-month is a single day, not a range or list of days. + *

+ * The 'L' and 'W' characters can also be combined for the day-of-month + * expression to yield 'LW', which translates to "last weekday of the + * month". + *

+ * The '#' character is allowed for the day-of-week field. This character is + * used to specify "the nth" XXX day of the month. For example, the + * value of "6#3" in the day-of-week field means the third Friday of + * the month (day 6 = Friday and "#3" = the 3rd one in the month). + * Other examples: "2#1" = the first Monday of the month and + * "4#5" = the fifth Wednesday of the month. Note that if you specify + * "#5" and there is not 5 of the given day-of-week in the month, then + * no firing will occur that month. If the '#' character is used, there can + * only be one expression in the day-of-week field ("3#1,6#3" is + * not valid, since there are two expressions). + *

+ * + *

+ * The legal characters and the names of months and days of the week are not + * case sensitive. + * + *

+ * NOTES: + *

    + *
  • Support for specifying both a day-of-week and a day-of-month value is + * not complete (you'll need to use the '?' character in one of these fields). + *
  • + *
  • Overflowing ranges is supported - that is, having a larger number on + * the left hand side than the right. You might do 22-2 to catch 10 o'clock + * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is + * very important to note that overuse of overflowing ranges creates ranges + * that don't make sense and no effort has been made to determine which + * interpretation CronExpression chooses. An example would be + * "0 0 14-6 ? * FRI-MON".
  • + *
+ *

+ * + * @author Sharada Jambula, James House + * @author Contributions from Mads Henderson + * @author Refactoring from CronTrigger to CronExpression by Aaron Craven + *

+ * Borrowed from quartz v2.3.1 + */ +public final class CronExpression { + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + private static final int SECOND = 0; + private static final int MINUTE = 1; + private static final int HOUR = 2; + private static final int DAY_OF_MONTH = 3; + private static final int MONTH = 4; + private static final int DAY_OF_WEEK = 5; + private static final int YEAR = 6; + private static final int ALL_SPEC_INT = 99; // '*' + private static final int NO_SPEC_INT = 98; // '?' + private static final Integer ALL_SPEC = ALL_SPEC_INT; + private static final Integer NO_SPEC = NO_SPEC_INT; + + private static final Map monthMap = new HashMap(20); + private static final Map dayMap = new HashMap(60); + + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TreeSet seconds; + private TreeSet minutes; + private TreeSet hours; + private TreeSet daysOfMonth; + private TreeSet months; + private TreeSet daysOfWeek; + private TreeSet years; + + private boolean lastdayOfWeek = false; + private int nthdayOfWeek = 0; + private boolean lastdayOfMonth = false; + private boolean nearestWeekday = false; + private int lastdayOffset = 0; + private boolean expressionParsed = false; + private TimeZone timeZone = null; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws ParseException if the string expression cannot be parsed into a valid + * CronExpression + */ + public CronExpression(String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } + + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { + + try { + new CronExpression(cronExpression); + } catch (ParseException pe) { + return false; + } + + return true; + } + + public static void validateExpression(String cronExpression) throws ParseException { + + new CronExpression(cronExpression); + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time after the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time after the given date/time which does + * not satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; + + // move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + // FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + // keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if (newDate == null) + break; + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this CronExpression + * will be resolved. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + /** + * Sets the time zone for which this CronExpression + * will be resolved. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the CronExpression + * + * @return a string representation of the CronExpression + */ + @Override + public String toString() { + return cronExpression; + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + + /// ///////////////////////////////////////////////////////////////////////// + + private void buildExpression(String expression) throws ParseException { + expressionParsed = true; + + try { + + if (seconds == null) { + seconds = new TreeSet<>(); + } + if (minutes == null) { + minutes = new TreeSet<>(); + } + if (hours == null) { + hours = new TreeSet<>(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet<>(); + } + if (months == null) { + months = new TreeSet<>(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet<>(); + } + if (years == null) { + years = new TreeSet<>(); + } + + int exprOn = SECOND; + + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet dow = getSet(DAY_OF_WEEK); + TreeSet dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e + ")", 0); + } + } + + private int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if (type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if (c == '-') { + ValueSet vs = getValue(0, s, i + 1); + lastdayOffset = vs.value; + if (lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i + 1); + i = vs.pos; + } + if (s.length() > i) { + c = s.charAt(i); + if (c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + private int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if (val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if (val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + + String buf = "seconds: " + + getExpressionSetSummary(seconds) + + "\n" + + "minutes: " + + getExpressionSetSummary(minutes) + + "\n" + + "hours: " + + getExpressionSetSummary(hours) + + "\n" + + "daysOfMonth: " + + getExpressionSetSummary(daysOfMonth) + + "\n" + + "months: " + + getExpressionSetSummary(months) + + "\n" + + "daysOfWeek: " + + getExpressionSetSummary(daysOfWeek) + + "\n" + + "lastdayOfWeek: " + + lastdayOfWeek + + "\n" + + "nearestWeekday: " + + nearestWeekday + + "\n" + + "NthDayOfWeek: " + + nthdayOfWeek + + "\n" + + "lastdayOfMonth: " + + lastdayOfMonth + + "\n" + + "years: " + + getExpressionSetSummary(years) + + "\n"; + + return buf; + } + + private String getExpressionSetSummary(java.util.Set set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + private String getExpressionSetSummary(java.util.ArrayList list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + private int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + } + + return i; + } + + private int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + } + + return i; + } + + private void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND: + max = 60; + break; + case MINUTE: + max = 60; + break; + case HOUR: + max = 24; + break; + case MONTH: + max = 12; + break; + case DAY_OF_WEEK: + max = 7; + break; + case DAY_OF_MONTH: + max = 31; + break; + case YEAR: + throw new IllegalArgumentException("Start year must be less than stop year"); + default: + throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } + + private ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + private int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + private int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + private int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + + /// ///////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + // if (endTime != null && cl.getTime().after(endTime)) return null; + if (cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if (!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if (t > day) { + mon++; + if (mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if (nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if (dow == Calendar.SATURDAY) { + day -= 1; + } else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if (dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = daysToAdd > 0; + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + private void setCalendarHour(Calendar cal, int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(Calendar.HOUR_OF_DAY, hour + 1); + } + } + + /** + * NOT YET IMPLEMENTED: Returns the time before the given time + * that the CronExpression matches. + */ + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + /** + * NOT YET IMPLEMENTED: Returns the final time that the + * CronExpression will match. + */ + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + private boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + private int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/FnHandle.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/FnHandle.java new file mode 100644 index 0000000..c2dcce6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/FnHandle.java @@ -0,0 +1,37 @@ +package com.njzscloud.common.sichen.support; + +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +@Slf4j +@Getter +@ToString +public class FnHandle { + private final String fn; + private final String defineName; + private final Object obj; + private final Method method; + + public FnHandle(String fn, Object obj, Method method) { + this.fn = fn; + this.obj = obj; + this.method = method; + this.defineName = obj.getClass().getCanonicalName() + "#" + method.getName(); + } + + public boolean exec() { + try { + if (obj == null || method == null) { + return false; + } + method.invoke(obj); + return true; + } catch (Throwable e) { + log.error("任务执行失败:{}", defineName, e); + return false; + } + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java new file mode 100644 index 0000000..acad4b1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/Task.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.sichen.support; + +import com.njzscloud.common.sichen.contant.TaskLogLevel; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; + +/** + * 将函数标记为定时任务执行函数,然后将类注册为 Spring Bean 即可,如果任务配置保存在数据库中,则所有参数都不用填,仅将方法作为执行函数 + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Task { + /** + * 任务 Id + */ + long id() default 0; + + /** + * 名称,默认为方法的全限定名 + */ + String name() default ""; + + /** + * 调度表达式 + */ + String cron() default ""; + + /** + * 周期,与表达式二选一,0 无效,都有值将以表达式为主 + */ + int period() default 0; + + /** + * 日志等级 + */ + TaskLogLevel logLevel() default TaskLogLevel.INFO; + + /** + * 备注 + */ + String memo() default ""; +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java new file mode 100644 index 0000000..6f79bce --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskHandle.java @@ -0,0 +1,188 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.core.util.IdUtil; +import com.njzscloud.common.core.thread.ThreadContext; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import com.njzscloud.common.sichen.contant.TaskStatus; +import com.njzscloud.common.sichen.service.TaskScheduleRecodeService; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; + +@Slf4j +@Getter +@Setter +@Accessors(chain = true) +public class TaskHandle implements Runnable { + /** + * 任务 Id + */ + private volatile long taskId; + /** + * 调度 Id + */ + private volatile long scheduleId; + /** + * 任务名称 + */ + private volatile String taskName; + /** + * 备注 + */ + private volatile String memo; + /** + * 调度方式; 字典代码:schedule_type + */ + private volatile ScheduleType scheduleType; + /** + * 调度配置; 手动时为空,固定周期时单位为秒 + */ + private volatile String scheduleConf; + /** + * 执行函数名 + */ + private volatile String fn; + /** + * 任务函数句柄 + */ + private FnHandle fnHandle; + /** + * 任务状态 + */ + private volatile TaskStatus taskStatus; + /** + * 调度时间 + */ + private volatile LocalDateTime scheduleTime; + /** + * 开始时间 + */ + private volatile LocalDateTime startTime; + /** + * 结束时间 + */ + private volatile LocalDateTime endTime; + /** + * 本次调度是否为手动触发 + */ + private volatile boolean manually; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + /** + * 是否为内内置任务 + */ + private boolean builtin; + private TaskScheduleRecodeService taskScheduleRecodeService; + + public TaskHandle(TaskScheduleRecodeService taskScheduleRecodeService, boolean manually, TaskInfo taskInfo) { + this.taskId = taskInfo.getId(); + this.scheduleId = IdUtil.getSnowflakeNextId(); + this.memo = taskInfo.getMemo(); + this.taskName = taskInfo.getTaskName(); + this.taskStatus = TaskStatus.Waiting; + this.fn = taskInfo.getFn(); + this.fnHandle = TaskStore.getFn(this.fn); + this.scheduleType = taskInfo.getScheduleType(); + this.scheduleConf = taskInfo.getScheduleConf(); + this.scheduleTime = LocalDateTime.now(); + this.taskScheduleRecodeService = taskScheduleRecodeService; + this.manually = manually; + this.logLevel = taskInfo.getLogLevel(); + this.builtin = taskInfo.isBuiltin(); + taskScheduleRecodeService.schedule(this); + this.logInfo("开始调度"); + } + + public static TaskHandle getTaskHandle() { + return ThreadContext.get("CURRENT_TASK"); + } + + public static void debug(String msg, Object... params) { + getTaskHandle().logDebug(msg, params); + } + + public static void info(String msg, Object... params) { + getTaskHandle().logInfo(msg, params); + } + + public static void warn(String msg, Object... params) { + getTaskHandle().logWarn(msg, params); + } + + public static void error(Throwable throwable, String msg, Object... params) { + getTaskHandle().logError(throwable, msg, params); + } + + public static void error(String msg, Object... params) { + getTaskHandle().logError(null, msg, params); + } + + @Override + public void run() { + ThreadContext.set("CURRENT_TASK", this); + try { + if (taskStatus != TaskStatus.Pending) { + log.warn("任务状态错误,无法运行,任务:{},当前状态:{}", taskName, taskStatus.getTxt()); + return; + } + taskStatus = TaskStatus.Running; + startTime = LocalDateTime.now(); + taskScheduleRecodeService.start(this); + if (fnHandle == null) { + endTime = LocalDateTime.now(); + taskStatus = TaskStatus.Error; + taskScheduleRecodeService.end(this); + log.error("任务执行失败:{},任务处理函数不存在:{}", taskName, fn); + return; + } + this.logInfo("开始执行"); + boolean exec = fnHandle.exec(); + endTime = LocalDateTime.now(); + taskStatus = exec ? TaskStatus.Completed : TaskStatus.Error; + taskScheduleRecodeService.end(this); + this.logInfo("执行完成"); + } catch (Exception e) { + log.error("错误:任务执行失败", e); + } finally { + ThreadContext.remove("CURRENT_TASK"); + } + } + + public void pending() { + this.taskStatus = TaskStatus.Pending; + taskScheduleRecodeService.pending(this); + } + + public void logDebug(String msg, Object... params) { + if (this.logLevel != TaskLogLevel.DEBUG) return; + taskScheduleRecodeService.log(this, TaskLogLevel.DEBUG, null, msg, params); + } + + public void logInfo(String msg, Object... params) { + if (this.logLevel != TaskLogLevel.DEBUG + && this.logLevel != TaskLogLevel.INFO) return; + taskScheduleRecodeService.log(this, TaskLogLevel.INFO, null, msg, params); + } + + public void logWarn(String msg, Object... params) { + if (this.logLevel != TaskLogLevel.DEBUG + && this.logLevel != TaskLogLevel.INFO + && this.logLevel != TaskLogLevel.WARN) return; + taskScheduleRecodeService.log(this, TaskLogLevel.WARN, null, msg, params); + } + + public void logError(Throwable throwable, String msg, Object... params) { + if (this.logLevel != TaskLogLevel.DEBUG + && this.logLevel != TaskLogLevel.INFO + && this.logLevel != TaskLogLevel.WARN + && this.logLevel != TaskLogLevel.ERROR) return; + taskScheduleRecodeService.log(this, TaskLogLevel.ERROR, throwable, msg, params); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java new file mode 100644 index 0000000..37e5931 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskInfo.java @@ -0,0 +1,50 @@ +package com.njzscloud.common.sichen.support; + +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.contant.TaskLogLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +@Setter +@Getter +@Slf4j +@ToString +@Accessors(chain = true) +public class TaskInfo { + private long id; + private String taskName; + private String fn; + private ScheduleType scheduleType; + private String scheduleConf; + private long criticalTiming; + private boolean disabled; + /** + * 是否为内内置任务 + */ + private boolean builtin; + private String memo; + + /** + * 日志等级 + */ + private TaskLogLevel logLevel; + + public TaskInfo() { + } + + public TaskInfo(TaskInfo taskInfo) { + this.id = taskInfo.id; + this.taskName = taskInfo.taskName; + this.fn = taskInfo.fn; + this.scheduleType = taskInfo.scheduleType; + this.scheduleConf = taskInfo.scheduleConf; + this.criticalTiming = taskInfo.criticalTiming; + this.disabled = taskInfo.disabled; + this.builtin = taskInfo.builtin; + this.memo = taskInfo.memo; + this.logLevel = taskInfo.logLevel; + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java new file mode 100644 index 0000000..d485389 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/support/TaskStore.java @@ -0,0 +1,45 @@ +package com.njzscloud.common.sichen.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public interface TaskStore { + Map TASK_FN = new HashMap<>(); + ReentrantReadWriteLock RW_LOCK = new ReentrantReadWriteLock(); + ReentrantReadWriteLock.ReadLock RLOCK = RW_LOCK.readLock(); + ReentrantReadWriteLock.WriteLock WLOCK = RW_LOCK.writeLock(); + + static void registerFn(String name, Object obj, Method method) { + TASK_FN.computeIfAbsent(name, k -> new FnHandle(k, obj, method)); + } + + static List> searchFn(String keywords) { + var entries = TASK_FN.entrySet(); + return entries.stream().filter(it -> StrUtil.isBlank(keywords) + || it.getKey().contains(keywords) + || it.getValue().getDefineName().contains(keywords)) + .map(it -> MapUtil.builder() + .put("fn", it.getKey()) + .put("defineName", it.getValue().getDefineName()) + .build()) + .toList(); + } + + static FnHandle getFn(String fn) { + return TASK_FN.get(fn); + } + + void registerTask(TaskInfo taskInfo); + + List loadTask(long soon); + + void updateCriticalTiming(long id, long criticalTiming); + + boolean trigger(Long id); +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TaskUtil.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TaskUtil.java new file mode 100644 index 0000000..19fec9f --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TaskUtil.java @@ -0,0 +1,151 @@ +package com.njzscloud.common.sichen.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sichen.contant.ScheduleType; +import com.njzscloud.common.sichen.executor.SichenExecutor; +import com.njzscloud.common.sichen.service.CombineStoreService; +import com.njzscloud.common.sichen.support.CronExpression; +import com.njzscloud.common.sichen.support.TaskInfo; +import lombok.extern.slf4j.Slf4j; + +import java.text.ParseException; +import java.util.Date; +import java.util.List; + +@Slf4j +public final class TaskUtil { + private static final CombineStoreService COMBINE_STORE_SERVICE = SpringUtil.getBean(CombineStoreService.class); + private final static SichenExecutor SICHEN_EXECUTOR = SpringUtil.getBean(SichenExecutor.class); + + /** + * 计算下次执行时间 + * + * @param now 当前时间(秒) + * @param taskInfo 任务 + */ + public static long computedNextTiming(long now, TaskInfo taskInfo) { + return computedNextTiming(now, taskInfo.getScheduleType(), taskInfo.getScheduleConf()); + } + + /** + * 计算下次执行时间 + * + * @param taskInfo 任务 + */ + public static long computedNextTiming(TaskInfo taskInfo) { + return computedNextTiming(System.currentTimeMillis() / 1000, taskInfo.getScheduleType(), taskInfo.getScheduleConf()); + } + + /** + * 计算下次执行时间 + * + * @param scheduleType 调度类型 + * @param scheduleConf 调度配置 + */ + public static long computedNextTiming(ScheduleType scheduleType, String scheduleConf) { + return computedNextTiming(System.currentTimeMillis() / 1000, scheduleType, scheduleConf); + } + + /** + * 计算下次执行时间 + * + * @param now 当前时间(秒) + * @param scheduleType 调度类型 + * @param scheduleConf 调度配置 + */ + public static long computedNextTiming(long now, ScheduleType scheduleType, String scheduleConf) { + switch (scheduleType) { + case Fixed: + return now + Long.parseLong(scheduleConf); + case Cron: + try { + return new CronExpression(scheduleConf) + .getNextValidTimeAfter(new Date(now * 1000)) + .getTime() / 1000; + } catch (ParseException e) { + log.error("任务调度表达式解析错误", e); + return 0; + } + case Manually: + return 0; + default: + throw Exceptions.error("调度类型错误:{}", scheduleType); + } + } + + /** + * 触发一次 + * + * @param id 任务 Id + */ + public static boolean trigger(Long id) { + return COMBINE_STORE_SERVICE.trigger(id); + } + + /** + * 注册任务(优先写入数据库) + * + * @param taskInfo 任务 + */ + public static void registerTask(TaskInfo taskInfo) { + Assert.notBlank(taskInfo.getTaskName(), () -> Exceptions.exception("任务名称不能为空")); + Assert.notBlank(taskInfo.getFn(), () -> Exceptions.exception("任务执行函数不能为空")); + boolean scheduleType = taskInfo.getScheduleType() != null; + Assert.isTrue(scheduleType, () -> Exceptions.exception("调度方式不能为空")); + Assert.isTrue(taskInfo.getScheduleType() == ScheduleType.Manually ? StrUtil.isBlank(taskInfo.getScheduleConf()) : StrUtil.isNotBlank(taskInfo.getScheduleConf()), () -> Exceptions.exception("调度方式或调度配置错误")); + COMBINE_STORE_SERVICE.registerTask(taskInfo); + } + + /** + * 注册任务 + * + * @param taskInfo 任务 + */ + public static void registerMemTask(TaskInfo taskInfo) { + Assert.notBlank(taskInfo.getTaskName(), () -> Exceptions.exception("任务名称不能为空")); + Assert.notBlank(taskInfo.getFn(), () -> Exceptions.exception("任务执行函数不能为空")); + boolean scheduleType = taskInfo.getScheduleType() != null; + Assert.isTrue(scheduleType, () -> Exceptions.exception("调度方式不能为空")); + Assert.isTrue(taskInfo.getScheduleType() == ScheduleType.Manually ? StrUtil.isBlank(taskInfo.getScheduleConf()) : StrUtil.isNotBlank(taskInfo.getScheduleConf()), () -> Exceptions.exception("调度方式或调度配置错误")); + COMBINE_STORE_SERVICE.memoryTaskStoreService.registerTask(taskInfo); + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public static void execute(TaskInfo task) { + SICHEN_EXECUTOR.execute(task, false); + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public static void execute(TaskInfo task, boolean manually) { + SICHEN_EXECUTOR.execute(task, manually); + } + + /** + * 执行任务 + * + * @param tasks 任务 + */ + public static void execute(List tasks) { + SICHEN_EXECUTOR.execute(tasks, false); + } + + /** + * 执行任务 + * + * @param tasks 任务 + */ + public static void execute(List tasks, boolean manually) { + SICHEN_EXECUTOR.execute(tasks, manually); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TimerUtil.java b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TimerUtil.java new file mode 100644 index 0000000..970dbcd --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/java/com/njzscloud/common/sichen/util/TimerUtil.java @@ -0,0 +1,85 @@ +package com.njzscloud.common.sichen.util; + +import com.njzscloud.common.core.thread.DefaultThreadFactory; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class TimerUtil { + private static ScheduledExecutorService scheduler; + + public static void init(boolean virtual, String poolName, int corePoolSize) { + scheduler = Executors.newScheduledThreadPool(virtual ? 0 : corePoolSize, new DefaultThreadFactory(virtual, poolName)); + log.info("定时器初始化成功"); + } + + public static void shutdown() { + if (scheduler != null) { + scheduler.shutdown(); + scheduler = null; + } + log.info("定时器已关闭"); + } + + /** + * 提交周期任务 + * + * @param period 周期(毫秒,以任务开始时间为准) + * @param task 任务 + */ + public static ScheduledFuture setInterval(long period, Runnable task) { + return setInterval(true, 0, period, task); + } + + /** + * 提交周期任务 + * + * @param delay 延迟(毫秒,首次执行) + * @param period 周期(毫秒,以任务开始时间为准) + * @param task 任务 + */ + public static ScheduledFuture setInterval(long delay, long period, Runnable task) { + return setInterval(true, delay, period, task); + } + + /** + * 提交周期任务 + * + * @param absolute 绝对周期,false-->以任务结束时间为准、true-->以任务开始时间为准 + * @param period 周期(毫秒) + * @param task 任务 + */ + public static ScheduledFuture setInterval(boolean absolute, long period, Runnable task) { + return setInterval(absolute, 0, period, task); + } + + /** + * 提交周期任务 + * + * @param absolute 绝对周期,false-->以任务结束时间为准、true-->以任务开始时间为准 + * @param delay 延迟(毫秒,首次执行) + * @param period 周期(毫秒) + * @param task 任务 + */ + public static ScheduledFuture setInterval(boolean absolute, long delay, long period, Runnable task) { + if (absolute) { + return scheduler.scheduleAtFixedRate(task, delay, period, TimeUnit.MILLISECONDS); + } else { + return scheduler.scheduleWithFixedDelay(task, delay, period, TimeUnit.MILLISECONDS); + } + } + + /** + * 提交延迟任务 + * + * @param delay 延迟(毫秒) + * @param task 任务 + */ + public static ScheduledFuture setTimeout(long delay, Runnable task) { + return scheduler.schedule(task, delay, TimeUnit.MILLISECONDS); + } +} diff --git a/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3ebb5d5 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.njzscloud.common.sichen.config.TaskAutoConfiguration +com.njzscloud.common.sichen.config.TimerAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-sichen/src/test/java/com/njzscloud/common/sichen/Example.java b/njzscloud-common/njzscloud-common-sichen/src/test/java/com/njzscloud/common/sichen/Example.java new file mode 100644 index 0000000..2837a7f --- /dev/null +++ b/njzscloud-common/njzscloud-common-sichen/src/test/java/com/njzscloud/common/sichen/Example.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.sichen; + +import cn.hutool.core.util.RandomUtil; +import com.njzscloud.common.sichen.support.Task; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +// @org.springframework.stereotype.Component +public class Example { + + @Task(period = 6) + public void a() { + Thread thread = Thread.currentThread(); + boolean virtual = thread.isVirtual(); + String name = thread.getName(); + log.info("A-------当前时间:{} 线程名称:{} 是否虚拟线程:{}", System.currentTimeMillis() / 1000, name, virtual); + try { + int sleepTime = RandomUtil.randomInt(1000, 5000); + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Task(period = 1) + public void b() { + log.info("B-------当前时间:{}", System.currentTimeMillis() / 1000); + } + + @Task(period = 1) + public void c() { + log.info("C-------当前时间:{}", System.currentTimeMillis() / 1000); + } + + @Task(cron = "0/1 * * * * ?") + public void d() { + log.info("D-------当前时间:{}", System.currentTimeMillis() / 1000); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/pom.xml b/njzscloud-common/njzscloud-common-sn/pom.xml new file mode 100644 index 0000000..a587586 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-sn + jar + + + 21 + 21 + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-mp + provided + + + com.njzscloud + njzscloud-common-mvc + + + diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java new file mode 100644 index 0000000..6854c35 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/AddSnConfigParam.java @@ -0,0 +1,48 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.HashMap; +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@Constraint +public class AddSnConfigParam implements Constrained { + /** + * 规则名称 + */ + @NotBlank(message = "规则名称不能为空") + private String snname; + /** + * 编码名称; 字典编码:sncode + */ + @NotBlank(message = "编码名称不能为空") + private String sncode; + + /** + * 配置 + */ + private List> config; + + /** + * 备注 + */ + private String memo; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> config != null && !config.isEmpty(), "编码规则不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnEntity.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnEntity.java new file mode 100644 index 0000000..e7b80d1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnEntity.java @@ -0,0 +1,98 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.sn.contant.PadMode; +import com.njzscloud.common.sn.contant.RollbackMode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 递增编码表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_inc_sn") +public class IncSnEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 代码(全局唯一) + */ + private String code; + + /** + * 当前数值 + */ + private Long val; + + /** + * 步长 + */ + private Integer step; + + /** + * 初始数值 + */ + private Long initialVal; + + /** + * 填充模式 + */ + private PadMode padMode; + + /** + * 填充值 + */ + private String padVal; + + /** + * 填充长度 + */ + private Integer padLen; + /** + * 最大数值 + */ + private Long maxVal; + /** + * 回卷模式 + */ + private RollbackMode rollback; + /** + * 是否允许溢出 + */ + private Boolean allowOverflow; + /** + * 上次时间 + */ + private LocalDateTime lastTime; + /** + * 上次批次 + */ + private String lastPici; + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + IncSnEntity that = (IncSnEntity) o; + return Objects.equals(code, that.code); + } + + @Override + public int hashCode() { + return Objects.hashCode(code); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnMapper.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnMapper.java new file mode 100644 index 0000000..979fb30 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 递增编码表 + */ +@Mapper +public interface IncSnMapper extends BaseMapper { + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnService.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnService.java new file mode 100644 index 0000000..1a6c629 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/IncSnService.java @@ -0,0 +1,205 @@ +package com.njzscloud.common.sn; + +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sn.contant.RollbackMode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; + +/** + * 递增编码表 + */ +@Slf4j +public class IncSnService extends ServiceImpl implements IService { + + /** + * 新增 + * + * @param incSnEntity 数据 + */ + public void add(IncSnEntity incSnEntity) { + this.save(incSnEntity); + } + + /** + * 修改 + * + * @param incSnEntity 数据 + */ + public void modify(IncSnEntity incSnEntity) { + this.updateById(incSnEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysIncCodeEntity 结果 + */ + public IncSnEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param incSnEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysIncCodeEntity> 分页结果 + */ + public PageResult paging(PageParam pageParam, IncSnEntity incSnEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(incSnEntity))); + } + + @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) + public Long inc(String code, String pici) { + IncSnEntity incCodeEntity = this.getOne(Wrappers.lambdaQuery() + .eq(IncSnEntity::getCode, code)); + Assert.notNull(incCodeEntity, () -> Exceptions.exception("编码规则不存在")); + long newVal = 0; + + long val = incCodeEntity.getVal(); + long maxVal = incCodeEntity.getMaxVal(); + + if (val == maxVal) { + Boolean allowOverflow = incCodeEntity.getAllowOverflow(); + if (allowOverflow == null) allowOverflow = Boolean.TRUE; + if (allowOverflow) { + newVal = incCodeEntity.getInitialVal(); + } else { + throw Exceptions.exception("编码已到最大值,规则:{},最大值:{}", code, maxVal); + } + } + + RollbackMode rollback = incCodeEntity.getRollback(); + if (rollback == null) rollback = RollbackMode.Wu; + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lastTime = incCodeEntity.getLastTime(); + if (lastTime == null) { + lastTime = now; + } + + switch (rollback) { + case Wu: { + newVal = val + incCodeEntity.getStep(); + } + break; + case Ri: { + if (now.isBefore(lastTime)) { + throw Exceptions.exception("服务器时间回溯,无法生成编号"); + } + LocalDateTime time1 = lastTime + .withHour(0).withMinute(0).withSecond(0).withNano(0); // 当天00:00:00 + + LocalDateTime time2 = lastTime + .withHour(23).withMinute(59).withSecond(59).withNano(999_999_999); // 当天23:59:59.999999999 + + if (time1.isBefore(time2)) { + newVal = incCodeEntity.getInitialVal(); + } else if (time1.isEqual(time2)) { + newVal = val + incCodeEntity.getStep(); + } + } + break; + case Zhou: { + if (now.isBefore(lastTime)) { + throw Exceptions.exception("服务器时间回溯,无法生成编号"); + } + LocalDateTime time1 = lastTime + .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + .withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); + LocalDateTime time2 = lastTime + .with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)) + .withHour(23) + .withMinute(59) + .withSecond(59) + .withNano(999_999_999); + + if (now.isBefore(time1) || now.isAfter(time2)) { + newVal = incCodeEntity.getInitialVal(); + } else { + newVal = val + incCodeEntity.getStep(); + } + } + break; + case Yue: { + if (now.isBefore(lastTime)) { + throw Exceptions.exception("服务器时间回溯,无法生成编号"); + } + LocalDateTime time1 = lastTime + .with(TemporalAdjusters.firstDayOfMonth()) + .withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime time2 = lastTime + .with(TemporalAdjusters.lastDayOfMonth()) + .withHour(23).withMinute(59).withSecond(59).withNano(999_999_999); + if (now.isBefore(time1) || now.isAfter(time2)) { + newVal = incCodeEntity.getInitialVal(); + } else { + newVal = val + incCodeEntity.getStep(); + } + } + break; + case Nian: { + if (now.isBefore(lastTime)) { + throw Exceptions.exception("服务器时间回溯,无法生成编号"); + } + LocalDateTime time1 = lastTime + .with(TemporalAdjusters.firstDayOfYear()) + .withHour(0).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime time2 = lastTime + .with(TemporalAdjusters.lastDayOfYear()) + .withHour(23).withMinute(59).withSecond(59).withNano(999_999_999); + if (now.isBefore(time1) || now.isAfter(time2)) { + newVal = incCodeEntity.getInitialVal(); + } else { + newVal = val + incCodeEntity.getStep(); + } + } + break; + case Pi: { + Assert.notBlank(pici, () -> Exceptions.exception("批次号不能为空")); + String lastPici = incCodeEntity.getLastPici(); + if (pici.equals(lastPici)) { + newVal = val + incCodeEntity.getStep(); + } else { + newVal = incCodeEntity.getInitialVal(); + } + } + break; + default: + throw Exceptions.exception("未知的回卷模式:{}", rollback); + } + + this.updateById(new IncSnEntity().setId(incCodeEntity.getId()) + .setLastTime(now) + .setLastPici(pici) + .setVal(newVal)); + return newVal; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java new file mode 100644 index 0000000..d7b7bc0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/ModifySnConfigParam.java @@ -0,0 +1,47 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.HashMap; +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@Constraint +public class ModifySnConfigParam implements Constrained { + private Long id; + + /** + * 规则名称 + */ + private String snname; + + /** + * 编码名称 + */ + private String sncode; + /** + * 配置 + */ + private List> config; + + /** + * 备注 + */ + private String memo; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> config != null && !config.isEmpty(), "编码规则不能为空"), + }; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SearchSnConfigResult.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SearchSnConfigResult.java new file mode 100644 index 0000000..9d38417 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SearchSnConfigResult.java @@ -0,0 +1,50 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.sn.support.SectionConfig; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 编码配置表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class SearchSnConfigResult { + + /** + * Id + */ + private Long id; + + /** + * 规则名称 + */ + private String snname; + + /** + * 示例 + */ + private String example; + + /** + * 编码名称 + */ + private String sncode; + + /** + * 配置 + */ + private List config; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigController.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigController.java new file mode 100644 index 0000000..1dd7e15 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigController.java @@ -0,0 +1,82 @@ +package com.njzscloud.common.sn; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sn.support.SnUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 编码配置表 + */ +@Slf4j +@RestController +@RequestMapping("/sys_sn_config") +@RequiredArgsConstructor +public class SnConfigController { + + private final SnConfigService snConfigService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@Validated @RequestBody AddSnConfigParam addSnConfigParam) { + snConfigService.add(addSnConfigParam); + return R.success(); + } + + @GetMapping("/next") + public R next(@RequestParam(required = false, defaultValue = "Default") String sncode, + @RequestParam(required = false) String pici) { + return R.success(SnUtil.next(sncode, pici)); + } + + @GetMapping("/reset") + public R reset(@RequestParam("sncode") String sncode) { + snConfigService.reset(sncode); + return R.success(); + } + + /** + * 修改 + * + * @param sysSnConfigEntity 数据 + */ + @PostMapping("/modify") + public R modify(@RequestBody ModifySnConfigParam modifySnConfigParam) { + snConfigService.modify(modifySnConfigParam); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + snConfigService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(snConfigService.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SnConfigEntity snConfigEntity) { + return R.success(snConfigService.paging(pageParam, snConfigEntity)); + } + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigEntity.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigEntity.java new file mode 100644 index 0000000..1e0d22e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigEntity.java @@ -0,0 +1,55 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.sn.support.SectionConfig; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 编码配置表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName(value = "sys_sn_config", autoResultMap = true) +public class SnConfigEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + /** + * 规则名称 + */ + private String snname; + + /** + * 示例 + */ + private String example; + /** + * 编码名称 + */ + private String sncode; + + /** + * 配置 + */ + @TableField(typeHandler = com.njzscloud.common.mp.support.handler.j.JsonTypeHandler.class) + private List config; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigMapper.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigMapper.java new file mode 100644 index 0000000..c7f1068 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigMapper.java @@ -0,0 +1,11 @@ +package com.njzscloud.common.sn; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 编码配置表 + */ +@Mapper +public interface SnConfigMapper extends BaseMapper { +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigService.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigService.java new file mode 100644 index 0000000..f5759d6 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/SnConfigService.java @@ -0,0 +1,234 @@ +package com.njzscloud.common.sn; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.tuple.Tuple2; +import com.njzscloud.common.core.utils.GroupUtil; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.sn.contant.PadMode; +import com.njzscloud.common.sn.support.IncSectionConfig; +import com.njzscloud.common.sn.support.SectionConfig; +import com.njzscloud.common.sn.support.Sn; +import com.njzscloud.common.sn.support.SnUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 编码配置表 + */ +@Slf4j +@RequiredArgsConstructor +public class SnConfigService extends ServiceImpl implements IService { + private final IncSnService incSnService; + + @Transactional(rollbackFor = Exception.class) + public Sn getSn(String sncode) { + SnConfigEntity configEntity = this.getOne(Wrappers.lambdaQuery().eq(SnConfigEntity::getSncode, sncode)); + if (configEntity == null) return new Sn(); + List configs = configEntity.getConfig(); + return new Sn(configs); + } + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public void add(AddSnConfigParam addSnConfigParam) { + String sncode = addSnConfigParam.getSncode(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(SnConfigEntity::getSncode, sncode)); + Assert.isFalse(exists, () -> Exceptions.exception("编码配置已存在:{}", sncode)); + List> configList = addSnConfigParam.getConfig(); + Tuple2, List> tuple2 = resolveConfig(configList); + + List configs = tuple2.get_0(); + String example = configs.stream().map(SectionConfig::genExample).collect(Collectors.joining()); + this.save(new SnConfigEntity() + .setSnname(addSnConfigParam.getSnname()) + .setSncode(sncode) + .setExample(example) + .setConfig(configs) + .setMemo(addSnConfigParam.getMemo()) + ); + + saveIncSnEntityList(tuple2.get_1()); + } + + private Tuple2, List> resolveConfig(List> configList) { + Assert.notEmpty(configList, () -> Exceptions.exception("编码配置不能为空")); + List configs = new ArrayList<>(configList.size()); + List incSnEntityList = new LinkedList<>(); + Set incSnEntityCodeList = new HashSet<>(); + for (HashMap config : configList) { + SectionConfig sectionConfig = SnUtil.resolve(config); + if (sectionConfig instanceof IncSectionConfig incSectionConfig) { + PadMode padMode = incSectionConfig.getPadMode(); + String code = incSectionConfig.getCode(); + Assert.isFalse(incSnEntityCodeList.contains(code), () -> Exceptions.exception("编码配置已存在:{}", code)); + incSnEntityCodeList.add(code); + incSnEntityList.add(new IncSnEntity() + .setCode(code) + .setVal((long) incSectionConfig.getInitialVal()) + .setStep(incSectionConfig.getStep()) + .setInitialVal((long) incSectionConfig.getInitialVal()) + .setMaxVal(padMode == PadMode.Wu ? Long.MAX_VALUE : (long) (Math.pow(10, incSectionConfig.getPadLen()) - 1)) + .setPadMode(padMode) + .setPadVal(incSectionConfig.getPadVal()) + .setPadLen(incSectionConfig.getPadLen()) + .setRollback(incSectionConfig.getRollback()) + .setAllowOverflow(incSectionConfig.getAllowOverflow())); + } + configs.add(sectionConfig); + } + + return Tuple2.of(configs, incSnEntityList); + } + + private void saveIncSnEntityList(List incSnEntityList) { + if (incSnEntityList.isEmpty()) return; + List incSnCodes = incSnEntityList.stream().map(IncSnEntity::getCode).collect(Collectors.toList()); + boolean exists = incSnService.exists(Wrappers.lambdaQuery().in(IncSnEntity::getCode, incSnCodes)); + Assert.isFalse(exists, () -> Exceptions.exception("编码配置已存在:{}", incSnCodes)); + incSnService.saveBatch(incSnEntityList); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(ModifySnConfigParam modifySnConfigParam) { + Long id = modifySnConfigParam.getId(); + SnConfigEntity oldData = this.getById(id); + Assert.notNull(oldData, () -> Exceptions.exception("编码配置不存在")); + List oldConfig = oldData.getConfig(); + List> newConfig = modifySnConfigParam.getConfig(); + Tuple2, List> tuple2 = resolveConfig(newConfig); + List oldIncSectionConfigCodeList = oldConfig.stream() + .filter(it -> it instanceof IncSectionConfig) + .map(it -> ((IncSectionConfig) it).getCode()) + .collect(Collectors.toList()); + List oldIncSnEntityList; + if (CollUtil.isEmpty(oldIncSectionConfigCodeList)) { + oldIncSnEntityList = Collections.emptyList(); + } else { + oldIncSnEntityList = incSnService.list(Wrappers.lambdaQuery(IncSnEntity.class).in(IncSnEntity::getCode, oldIncSectionConfigCodeList)); + } + modifyIncSnSectionConfig(oldIncSnEntityList, tuple2.get_1()); + List configs = tuple2.get_0(); + String example = configs.stream().map(SectionConfig::genExample).collect(Collectors.joining()); + this.updateById(new SnConfigEntity() + .setId(id) + .setSnname(modifySnConfigParam.getSnname()) + .setSncode(modifySnConfigParam.getSncode()) + .setExample(example) + .setConfig(tuple2.get_0()) + .setMemo(modifySnConfigParam.getMemo()) + ); + } + + private void modifyIncSnSectionConfig(List oldIncSnEntityList, List newIncSnEntityList) { + if (oldIncSnEntityList.isEmpty() && newIncSnEntityList.isEmpty()) return; + // 旧无 新有 --> 新增 + List addList = difference(newIncSnEntityList, oldIncSnEntityList); + if (CollUtil.isNotEmpty(addList)) saveIncSnEntityList(addList); + // 旧有 新有 --> 修改 + List modList = intersection(oldIncSnEntityList, newIncSnEntityList); + if (CollUtil.isNotEmpty(modList)) incSnService.updateBatchById(modList); + // 旧有 新无 --> 删除 + List delList = difference(oldIncSnEntityList, newIncSnEntityList); + if (CollUtil.isNotEmpty(delList)) incSnService.removeBatchByIds(delList); + } + + private List difference(List list1, List list2) { + List codes = list2.stream().map(IncSnEntity::getCode).collect(Collectors.toList()); + return list1.stream() + .filter(it -> !codes.contains(it.getCode())) + .collect(Collectors.toList()); + } + + private List intersection(List list1, List list2) { + Map map = GroupUtil.k_o(list2, IncSnEntity::getCode); + return list1.stream() + .filter(it -> map.containsKey(it.getCode())) + .map(it -> { + IncSnEntity newData = map.get(it.getCode()); + PadMode padMode = newData.getPadMode(); + Integer padLen = newData.getPadLen(); + return new IncSnEntity() + .setId(it.getId()) + .setStep(newData.getStep()) + .setInitialVal(newData.getInitialVal()) + .setPadMode(padMode) + .setPadVal(newData.getPadVal()) + .setPadLen(padLen) + .setMaxVal(padMode == PadMode.Wu ? Long.MAX_VALUE : (long) (Math.pow(10, padLen) - 1)) + .setRollback(newData.getRollback()) + .setAllowOverflow(newData.getAllowOverflow()); + }) + .collect(Collectors.toList()); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public SnConfigEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, SnConfigEntity snConfigEntity) { + String sncode = snConfigEntity.getSncode(); + String snname = snConfigEntity.getSnname(); + LambdaQueryWrapper ew = Wrappers.lambdaQuery(SnConfigEntity.class) + .like(StrUtil.isNotBlank(sncode), SnConfigEntity::getSncode, sncode) + .like(StrUtil.isNotBlank(snname), SnConfigEntity::getSnname, snname); + IPage page = this.page(pageParam.toPage(), ew); + return PageResult.of(page.convert(it -> BeanUtil.copyProperties(it, SearchSnConfigResult.class))); + } + + @Transactional(rollbackFor = Exception.class) + public void reset(String sncode) { + SnConfigEntity configEntity = this.getOne(Wrappers.lambdaQuery(SnConfigEntity.class).eq(SnConfigEntity::getSncode, sncode)); + Assert.notNull(configEntity, () -> Exceptions.clierr("规则不存在")); + List config = configEntity.getConfig(); + List codes = config.stream().filter(it -> it instanceof IncSectionConfig) + .map(it -> ((IncSectionConfig) it).getCode()) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(codes)) return; + + List incSnEntityList = incSnService + .list(Wrappers.lambdaQuery(IncSnEntity.class).in(IncSnEntity::getCode, codes)) + .stream().map(it -> BeanUtil.copyProperties(it, IncSnEntity.class) + .setVal(it.getInitialVal()) + .setLastTime(null) + .setLastPici(null) + ) + .collect(Collectors.toList()); + + incSnService.remove(Wrappers.lambdaQuery(IncSnEntity.class).in(IncSnEntity::getCode, codes)); + + incSnService.saveBatch(incSnEntityList); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java new file mode 100644 index 0000000..f04df06 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.njzscloud.common.sn.config; + +import com.njzscloud.common.sn.IncSnService; +import com.njzscloud.common.sn.SnConfigController; +import com.njzscloud.common.sn.SnConfigService; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan("com.njzscloud.common.sn") +@EnableConfigurationProperties(SnProperties.class) +public class SnAutoConfiguration { + + @Bean + public IncSnService sysIncSnService() { + return new IncSnService(); + } + + @Bean + public SnConfigService sysSnConfigService(IncSnService incSnService) { + return new SnConfigService(incSnService); + } + + @Bean + public SnConfigController sysSnConfigController(SnConfigService snConfigService) { + return new SnConfigController(snConfigService); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java new file mode 100644 index 0000000..e55b7b7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/config/SnProperties.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.sn.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@ConfigurationProperties(prefix = "sn") +public class SnProperties { +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java new file mode 100644 index 0000000..0bcbf69 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/PadMode.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:pad_mode + * 字典名称:填充模式 + */ +@Getter +@RequiredArgsConstructor +public enum PadMode implements DictStr { + Wu("Wu", "无"), + Zuo("Zuo", "左"), + You("You", "右"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java new file mode 100644 index 0000000..4f43e8b --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RandomMode.java @@ -0,0 +1,21 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:random_mode + * 字典名称:随机模式 + */ +@Getter +@RequiredArgsConstructor +public enum RandomMode implements DictStr { + SnowFlake("SnowFlake", "雪花码"), + UUID("UUID", "UUID"), + NanoId("NanoId", "NanoId"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RollbackMode.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RollbackMode.java new file mode 100644 index 0000000..4d0979c --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/RollbackMode.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:rollback_mode + * 字典名称:自增段回滚模式 + */ +@Getter +@RequiredArgsConstructor +public enum RollbackMode implements DictStr { + Wu("Wu", "不回卷"), + Ri("Ri", "每日"), + Zhou("Zhou", "每周"), + Yue("Yue", "每月"), + Nian("Nian", "每年"), + Pi("Pi", "每批"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java new file mode 100644 index 0000000..2187081 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/contant/SnSection.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.sn.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:sn_section + * 字典名称:编码类型 + */ +@Getter +@RequiredArgsConstructor +public enum SnSection implements DictStr { + GuDing("GuDing", "固定"), + ShiJian("ShiJian", "时间"), + ZiZeng("ZiZeng", "自增"), + SuiJi("SuiJi", "随机"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java new file mode 100644 index 0000000..9e8b1a0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSection.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.sn.support; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class FixedSection implements ISnSection { + /** + * 固定值 + */ + private final String value; + + @Override + public String next(String pici) { + return value; + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java new file mode 100644 index 0000000..25e6cc3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/FixedSectionConfig.java @@ -0,0 +1,39 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@Accessors(chain = true) +public class FixedSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.GuDing; + /** + * 固定值 + */ + private String value; + + + @Override + public String genExample() { + return value; + } + + @Override + public ISnSection section() { + return new FixedSection(value); + } + + @Override + public void resolve(Map config) { + value = MapUtil.getStr(config, "value"); + Assert.notBlank(value, () -> Exceptions.clierr("固定编码配置错误,未设置字段 value")); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java new file mode 100644 index 0000000..3d8b678 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/ISnSection.java @@ -0,0 +1,5 @@ +package com.njzscloud.common.sn.support; + +public interface ISnSection { + String next(String pici); +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java new file mode 100644 index 0000000..622ce78 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSection.java @@ -0,0 +1,56 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.sn.IncSnService; +import com.njzscloud.common.sn.contant.PadMode; +import com.njzscloud.common.sn.contant.RollbackMode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class IncSection implements ISnSection { + /** + * 编码段编号 + */ + private final String code; + /** + * 步长 + */ + private final Integer step; + /** + * 初始数值 + */ + private final Integer initialVal; + /** + * 填充模式 + */ + private final PadMode padMode; + /** + * 填充值 + */ + private final String padVal; + /** + * 填充长度 + */ + private final Integer padLen; + private final IncSnService incSnService = SpringUtil.getBean(IncSnService.class); + /** + * 回卷模式 + */ + private RollbackMode rollback; + /** + * 是否允许溢出 + */ + private Boolean allowOverflow; + + @Override + public String next(String pici) { + Long val = incSnService.inc(code, pici); + return padMode == PadMode.Zuo ? StrUtil.padPre(val.toString(), padLen, padVal) : + padMode == PadMode.You ? StrUtil.padAfter(val.toString(), padLen, padVal) : + val.toString(); + } + +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java new file mode 100644 index 0000000..594d2f2 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/IncSectionConfig.java @@ -0,0 +1,87 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.contant.PadMode; +import com.njzscloud.common.sn.contant.RollbackMode; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@Accessors(chain = true) +public class IncSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.ZiZeng; + /** + * 编码段编号 + */ + private String code; + /** + * 步长 + */ + private Integer step; + /** + * 初始数值 + */ + private Integer initialVal; + /** + * 填充模式 + */ + private PadMode padMode; + /** + * 填充值 + */ + private String padVal; + /** + * 填充长度 + */ + private Integer padLen; + /** + * 回卷模式 + */ + private RollbackMode rollback; + /** + * 是否允许溢出 + */ + private Boolean allowOverflow; + + @Override + public String genExample() { + return padMode == PadMode.Zuo ? StrUtil.padPre(initialVal.toString(), padLen, padVal) : + padMode == PadMode.You ? StrUtil.padAfter(initialVal.toString(), padLen, padVal) : + initialVal.toString(); + } + + @Override + public ISnSection section() { + return new IncSection(code, step, initialVal, padMode, padVal, padLen); + } + + @Override + public void resolve(Map config) { + code = MapUtil.getStr(config, "code"); + step = MapUtil.getInt(config, "step"); + initialVal = MapUtil.getInt(config, "initialVal"); + rollback = Dict.parse(MapUtil.getStr(config, "rollback"), RollbackMode.values()); + if (rollback == null) rollback = RollbackMode.Wu; + allowOverflow = MapUtil.getBool(config, "allowOverflow", Boolean.TRUE); + padMode = Dict.parse(MapUtil.getStr(config, "padMode"), PadMode.values()); + if (padMode == null) padMode = PadMode.Wu; + padVal = MapUtil.getStr(config, "padVal"); + padLen = MapUtil.getInt(config, "padLen"); + Assert.notBlank(code, () -> Exceptions.clierr("递增编码配置错误,未设置字段 code")); + Assert.notNull(step, () -> Exceptions.clierr("递增编码配置错误,未设置字段 step")); + Assert.notNull(initialVal, () -> Exceptions.clierr("递增编码配置错误,未设置字段 initialVal")); + if (padMode != PadMode.Wu) { + Assert.notBlank(padVal, () -> Exceptions.clierr("递增编码配置错误,未设置字段 padVal")); + Assert.notNull(padLen, () -> Exceptions.clierr("递增编码配置错误,未设置字段 padLen")); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java new file mode 100644 index 0000000..5e6570e --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSection.java @@ -0,0 +1,54 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.util.IdUtil; +import com.njzscloud.common.sn.contant.RandomMode; + +public class RandomSection implements ISnSection { + /** + * 随机模式 + */ + private final RandomMode randomMode; + /** + * 雪花码-工作节点ID + */ + private final Long workerId; + /** + * 雪花码-数据中心ID + */ + private final Long datacenterId; + /** + * NanoId码-大小 + */ + private final Integer nanoIdSize; + + public RandomSection(RandomMode randomMode, Long workerId, Long datacenterId, Integer nanoIdSize) { + this.workerId = workerId; + this.datacenterId = datacenterId; + this.nanoIdSize = nanoIdSize; + this.randomMode = randomMode; + } + + public RandomSection(Long workerId, Long datacenterId) { + this(RandomMode.SnowFlake, workerId, datacenterId, 21); + } + + public RandomSection(Integer nanoIdSize) { + this(RandomMode.NanoId, null, null, nanoIdSize); + } + + public RandomSection() { + this(RandomMode.UUID, null, null, null); + } + + @Override + public String next(String pici) { + switch (randomMode) { + case UUID: + return IdUtil.simpleUUID(); + case NanoId: + return nanoIdSize == null ? IdUtil.nanoId() : IdUtil.nanoId(nanoIdSize); + default: + return workerId == null || datacenterId == null ? IdUtil.getSnowflakeNextIdStr() : IdUtil.getSnowflake(workerId, datacenterId).nextIdStr(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java new file mode 100644 index 0000000..8d019f7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/RandomSectionConfig.java @@ -0,0 +1,76 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.contant.RandomMode; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Getter +@Setter +@Accessors(chain = true) +public class RandomSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.SuiJi; + /** + * 随机模式 + */ + private RandomMode randomMode; + /** + * 雪花码-工作节点ID + */ + private Long workerId; + /** + * 雪花码-数据中心ID + */ + private Long datacenterId; + /** + * NanoId码-大小 + */ + private Integer nanoIdSize; + + @Override + public String genExample() { + switch (randomMode) { + case UUID: + return IdUtil.simpleUUID(); + case NanoId: + return nanoIdSize == null ? IdUtil.nanoId() : IdUtil.nanoId(nanoIdSize); + default: + return workerId == null || datacenterId == null ? IdUtil.getSnowflakeNextIdStr() : IdUtil.getSnowflake(workerId, datacenterId).nextIdStr(); + } + } + + @Override + public ISnSection section() { + return new RandomSection(randomMode, workerId, datacenterId, nanoIdSize); + } + + @Override + public void resolve(Map config) { + randomMode = Dict.parse(MapUtil.getStr(config, "randomMode"), RandomMode.values()); + workerId = MapUtil.getLong(config, "workerId"); + datacenterId = MapUtil.getLong(config, "datacenterId"); + nanoIdSize = MapUtil.getInt(config, "nanoIdSize"); + Assert.notNull(randomMode, () -> Exceptions.clierr("随机编码配置错误,未设置字段 randomMode")); + if (randomMode == RandomMode.NanoId) { + Assert.notNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,未设置字段 nanoIdSize")); + Assert.isNull(workerId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 workerId")); + Assert.isNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 datacenterId")); + } else if (randomMode == RandomMode.SnowFlake) { + Assert.notNull(workerId, () -> Exceptions.clierr("随机编码配置错误,未设置字段 workerId")); + Assert.notNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,未设置字段 datacenterId")); + Assert.isNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 nanoIdSize")); + } else { + Assert.isNull(workerId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 workerId")); + Assert.isNull(datacenterId, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 datacenterId")); + Assert.isNull(nanoIdSize, () -> Exceptions.clierr("随机编码配置错误,无需设置字段 nanoIdSize")); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java new file mode 100644 index 0000000..0dcf599 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SectionConfig.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.sn.support; + + +import com.njzscloud.common.sn.contant.SnSection; + +import java.util.Map; + +public interface SectionConfig { + + /** + * 编码段名称 + */ + SnSection getSectionName(); + + String genExample(); + + /** + * 编码段实例 + */ + ISnSection section(); + + /** + * 解析配置 + */ + void resolve(Map config); +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java new file mode 100644 index 0000000..3e7db98 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/Sn.java @@ -0,0 +1,46 @@ +package com.njzscloud.common.sn.support; + +import com.njzscloud.common.core.ex.Exceptions; + +import java.util.List; +import java.util.stream.Collectors; + +public class Sn { + private final List sections; + + public Sn(List configs) { + this.sections = configs.stream().map(SectionConfig::section).collect(Collectors.toList()); + } + + public Sn() { + this.sections = null; + } + + public String next() { + return next(null); + } + + /** + * 下一个编码 + * + * @param pici 批次 + * @return 下一个编码 + */ + public String next(String pici) { + if (sections == null || sections.isEmpty()) { + return null; + } + + StringBuilder builder = new StringBuilder(); + + try { + for (ISnSection section : sections) { + builder.append(section.next(pici)); + } + } catch (Exception e) { + throw Exceptions.error("编码配置配置错误"); + } + + return builder.toString(); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java new file mode 100644 index 0000000..e769203 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/SnUtil.java @@ -0,0 +1,51 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.ienum.Dict; +import com.njzscloud.common.sn.SnConfigService; +import com.njzscloud.common.sn.contant.SnSection; + +import java.util.HashMap; +import java.util.Map; + +public final class SnUtil { + + private static final SnConfigService SN_CONFIG_SERVICE = SpringUtil.getBean(SnConfigService.class); + private static final Map> SECTIONS = new HashMap<>(); + + static { + SECTIONS.put(SnSection.GuDing, FixedSectionConfig.class); + SECTIONS.put(SnSection.ShiJian, TimeSectionConfig.class); + SECTIONS.put(SnSection.ZiZeng, IncSectionConfig.class); + SECTIONS.put(SnSection.SuiJi, RandomSectionConfig.class); + } + + public static SectionConfig resolve(Map config) { + String sectionName = MapUtil.getStr(config, "sectionName"); + Class sectionClass = SECTIONS.get(Dict.parse(sectionName, SnSection.values())); + SectionConfig sectionConfig; + try { + sectionConfig = sectionClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw Exceptions.error("配置解析失败"); + } + sectionConfig.resolve(config); + return sectionConfig; + } + + public static String next() { + return next("Default"); + } + + public static String next(String sncode) { + return next(sncode, null); + } + + public static synchronized String next(String sncode, String pici) { + Assert.notBlank(sncode, () -> Exceptions.clierr("未指定编码规则")); + return SN_CONFIG_SERVICE.getSn(sncode).next(pici); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java new file mode 100644 index 0000000..7c25db1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSection.java @@ -0,0 +1,27 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.date.DateUtil; +import lombok.RequiredArgsConstructor; + +import java.util.Date; + +@RequiredArgsConstructor +public class TimeSection implements ISnSection { + /** + * 时间格式 + */ + private final String pattern; + /** + * 是否返回时间戳 + */ + private final Boolean timestamp; + /** + * 时间单位,1 --> 毫秒,1000-->秒,60000 --> 分,3600000 --> 时,86400000 --> 天 + */ + private final Integer unit; + + @Override + public String next(String pici) { + return timestamp ? new Date().getTime() / unit + "" : DateUtil.format(new Date(), pattern); + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java new file mode 100644 index 0000000..e1a31a3 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/java/com/njzscloud/common/sn/support/TimeSectionConfig.java @@ -0,0 +1,60 @@ +package com.njzscloud.common.sn.support; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.sn.contant.SnSection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.Map; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TimeSectionConfig implements SectionConfig { + private final SnSection sectionName = SnSection.ShiJian; + /** + * 时间格式 + */ + private String pattern; + /** + * 是否返回时间戳 + */ + private Boolean timestamp; + /** + * 时间单位,1 --> 毫秒,1000-->秒,60000 --> 分,3600000 --> 时,86400000 --> 天 + */ + private Integer unit; + + @Override + public String genExample() { + return timestamp ? new Date().getTime() / unit + "" : DateUtil.format(new Date(), pattern); + } + + @Override + public ISnSection section() { + return new TimeSection(pattern, timestamp, unit); + } + + @Override + public void resolve(Map config) { + pattern = MapUtil.getStr(config, "pattern"); + timestamp = MapUtil.getBool(config, "timestamp", false); + unit = MapUtil.getInt(config, "unit"); + if (timestamp) { + Assert.isTrue(unit != null && (unit == 1 || unit == 1000 || unit == 60000 || unit == 3600000 || unit == 86400000), () -> Exceptions.clierr("时间编码配置错误,字段 unit 只能为 1,1000,60000,3600000,86400000 中的一个")); + Assert.isTrue(StrUtil.isBlank(pattern), () -> Exceptions.clierr("时间编码配置错误,字段 pattern 与 timestamp 不能同时使用")); + + } else { + Assert.notBlank(pattern, () -> Exceptions.clierr("时间编码配置错误,未设置字段 pattern")); + Assert.isTrue(unit == null, () -> Exceptions.clierr("时间编码配置错误,字段 unit 与 pattern 不能同时使用")); + } + } +} diff --git a/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..55907d9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sn/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.sn.config.SnAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-sshtunnel/pom.xml b/njzscloud-common/njzscloud-common-sshtunnel/pom.xml new file mode 100644 index 0000000..8dbc309 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sshtunnel/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-sshtunnel + jar + + njzscloud-common-sshtunnel + http://maven.apache.org + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.jcraft + jsch + 0.1.55 + + + diff --git a/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnel.java b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnel.java new file mode 100644 index 0000000..7bc49e8 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnel.java @@ -0,0 +1,129 @@ +package com.njzscloud.common.sshtunnel; + +import cn.hutool.core.util.StrUtil; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.njzscloud.common.core.tuple.Tuple2; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +public class SSHTunnel { + + private final List> tunnels = new LinkedList<>(); + private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor(); + private final AtomicBoolean running = new AtomicBoolean(false); + + public SSHTunnel(SSHTunnelProperties sshTunnelProperties) { + if (sshTunnelProperties.isEnable()) { + log.info("SSH 隧道已启用"); + running.set(true); + List tunnelsProperties = sshTunnelProperties.getTunnels(); + tunnelsProperties.stream() + .filter(SSHTunnelProperties.SSHProperties::isEnable) + .forEach(it -> { + Tuple2 tunnel = createTunnel(it); + tunnels.add(tunnel); + }); + reconnectTask(); + } + } + + private Tuple2 createTunnel(SSHTunnelProperties.SSHProperties sshProperties) { + try { + String host = sshProperties.getHost(); + int port = sshProperties.getPort(); + String user = sshProperties.getUser(); + String pwd = sshProperties.getPwd(); + String credentials = sshProperties.getCredentials(); + String passphrase = sshProperties.getPassphrase(); + log.info("正在连接服务器:{}:{}", host, port); + JSch jsch = new JSch(); + Session session; + if (StrUtil.isNotBlank(credentials)) { + // 使用私钥认证 + File keyFile = new File(credentials); + if (!keyFile.exists()) { + throw new RuntimeException("私钥文件不存在: " + credentials); + } + + if (passphrase != null) { + jsch.addIdentity(credentials, passphrase); + } else { + jsch.addIdentity(credentials); + } + session = jsch.getSession(user, host, port); + } else if (StrUtil.isNotBlank(pwd)) { + // 创建SSH会话 + session = jsch.getSession(user, host, port); + // 使用密码认证 + session.setPassword(pwd); + } else { + throw new RuntimeException("未提供有效认证方式(私钥或密码)"); + } + + + session.setConfig("StrictHostKeyChecking", "no"); // 禁用主机密钥检查 + + // 设置保持活动状态 + session.setServerAliveInterval(60000); // 每分钟发送一次保持活动的数据包 + session.setServerAliveCountMax(3); // 允许3次失败 + + session.connect(); + + List proxyProperties = sshProperties.getProxy(); + for (SSHTunnelProperties.ProxyProperties proxyProperty : proxyProperties) { + int localPort = proxyProperty.getLocalPort(); + String targetHost = proxyProperty.getTargetHost(); + int targetPort = proxyProperty.getTargetPort(); + // 设置端口转发 (本地端口 -> 目标地址) + session.setPortForwardingL(localPort, targetHost, targetPort); + log.info("已创建隧道,本地端口:{} --> 服务器:{}:{} --> 目标地址:{}:{}", localPort, host, port, targetHost, targetPort); + } + return Tuple2.of(jsch, session); + } catch (JSchException e) { + log.error("SSH 隧道连接失败", e); + throw new RuntimeException("SSH 隧道连接失败", e); + } + } + + private void reconnectTask() { + reconnectExecutor.scheduleAtFixedRate(() -> { + if (!running.get()) return; + try { + for (Tuple2 tunnel : tunnels) { + Session session = tunnel.get_1(); + if (!session.isConnected()) { + log.warn("SSH 隧道连接已断开,尝试重新连接..."); + session.connect(); + } + } + } catch (Exception e) { + log.error("重连任务执行失败", e); + } + }, 30, 30, TimeUnit.SECONDS); // 每30秒检查一次连接状态 + } + + public void close() { + running.set(false); + reconnectExecutor.shutdownNow(); + for (Tuple2 tunnel : tunnels) { + Session session = tunnel.get_1(); + if (session != null && session.isConnected()) { + String host = session.getHost(); + int port = session.getPort(); + session.disconnect(); + log.info("SSH 隧道已关闭,服务器:{}:{}", host, port); + } + } + tunnels.clear(); + } +} diff --git a/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelAutoConfiguration.java b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelAutoConfiguration.java new file mode 100644 index 0000000..ee14106 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.njzscloud.common.sshtunnel; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBooleanProperty(prefix = "ssh-tunnel", name = "enable") +@EnableConfigurationProperties({SSHTunnelProperties.class}) +public class SSHTunnelAutoConfiguration { + + @Bean(destroyMethod = "close") + public SSHTunnel sshTunnel(SSHTunnelProperties SSHTunnelProperties) { + return new SSHTunnel(SSHTunnelProperties); + } + +} diff --git a/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelProperties.java b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelProperties.java new file mode 100644 index 0000000..a0000df --- /dev/null +++ b/njzscloud-common/njzscloud-common-sshtunnel/src/main/java/com/njzscloud/common/sshtunnel/SSHTunnelProperties.java @@ -0,0 +1,66 @@ +package com.njzscloud.common.sshtunnel; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Getter +@Setter +@ConfigurationProperties("ssh-tunnel") +public class SSHTunnelProperties { + private boolean enable = false; + + private List tunnels; + + @Getter + @Setter + public static class SSHProperties { + private boolean enable = true; + + /** + * 服务器地址 + */ + private String host; + /** + * SSH 端口 + */ + private int port = 22; + /** + * 用户名 + */ + private String user; + /** + * 密码 + */ + private String pwd; + /** + * 秘钥文件 + */ + private String credentials; + /** + * 私钥密码 + */ + private String passphrase; + private List proxy; + } + + @Getter + @Setter + public static class ProxyProperties { + /** + * 本地端口 + */ + private int localPort; + /** + * 目标主机 + */ + private String targetHost; + /** + * 目标端口 + */ + private int targetPort; + } + +} diff --git a/njzscloud-common/njzscloud-common-sshtunnel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-sshtunnel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..19cbad9 --- /dev/null +++ b/njzscloud-common/njzscloud-common-sshtunnel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.sshtunnel.SSHTunnelAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-wechat/pom.xml b/njzscloud-common/njzscloud-common-wechat/pom.xml new file mode 100644 index 0000000..74ef09c --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-wechat + jar + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-http + provided + + + diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatApi.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatApi.java new file mode 100644 index 0000000..05e88ff --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatApi.java @@ -0,0 +1,25 @@ +package com.njzscloud.common.wechat; + +import com.njzscloud.common.http.annotation.GetEndpoint; +import com.njzscloud.common.http.annotation.QueryParam; +import com.njzscloud.common.http.annotation.RemoteServer; +import com.njzscloud.common.wechat.param.Code2SessionParam; +import com.njzscloud.common.wechat.param.GetAccessTokenParam; +import com.njzscloud.common.wechat.result.Code2SessionResult; +import com.njzscloud.common.wechat.result.GetAccessTokenResult; + +@RemoteServer(value = "${wechat.base-url}") +public interface WechatApi { + + /** + * 获取access_token + */ + @GetEndpoint("/cgi-bin/token") + GetAccessTokenResult getAccessToken(@QueryParam GetAccessTokenParam getAccessTokenParam); + + /** + * 小程序登录 + */ + @GetEndpoint("/sns/jscode2session") + Code2SessionResult code2Session(@QueryParam Code2SessionParam code2SessionParam); +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatUtil.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatUtil.java new file mode 100644 index 0000000..0679682 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/WechatUtil.java @@ -0,0 +1,38 @@ +package com.njzscloud.common.wechat; + + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.wechat.param.Code2SessionParam; +import com.njzscloud.common.wechat.param.GetAccessTokenParam; +import com.njzscloud.common.wechat.result.Code2SessionResult; +import com.njzscloud.common.wechat.result.GetAccessTokenResult; + +import java.util.Date; + +public class WechatUtil { + private static final WechatApi API; + private static String accessToken = ""; + private static long tokenExpTime = 0; + + static { + API = SpringUtil.getBean(WechatApi.class); + } + + public static Code2SessionResult code2Session(Code2SessionParam param) { + auth(); + return API.code2Session(param.setAccess_token(accessToken)); + } + + public static void auth() { + if (tokenExpTime <= new Date().getTime() / 1000 + 60) { + GetAccessTokenResult getAccessTokenResult = API.getAccessToken(new GetAccessTokenParam()); + accessToken = getAccessTokenResult.getAccess_token(); + Integer expiresIn = getAccessTokenResult.getExpires_in(); + Assert.notBlank(accessToken, () -> Exceptions.error("微信登录失败")); + Assert.notNull(expiresIn, () -> Exceptions.error("微信登录失败")); + tokenExpTime = new Date().getTime() / 1000 + expiresIn; + } + } +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatAutoConfiguration.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatAutoConfiguration.java new file mode 100644 index 0000000..5fd6da7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatAutoConfiguration.java @@ -0,0 +1,16 @@ +package com.njzscloud.common.wechat.config; + +import com.njzscloud.common.http.HttpClientDecorator; +import com.njzscloud.common.wechat.WechatApi; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(WechatProperties.class) +public class WechatAutoConfiguration { + @Bean + public WechatApi wechatApi(HttpClientDecorator decorator) { + return decorator.decorate(WechatApi.class); + } +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatProperties.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatProperties.java new file mode 100644 index 0000000..4b4e923 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/config/WechatProperties.java @@ -0,0 +1,18 @@ +package com.njzscloud.common.wechat.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@ConfigurationProperties(prefix = "wechat") +public class WechatProperties { + private String appId; + private String appSecret; + private String baseUrl; +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/Code2SessionParam.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/Code2SessionParam.java new file mode 100644 index 0000000..2976ce7 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/Code2SessionParam.java @@ -0,0 +1,26 @@ +package com.njzscloud.common.wechat.param; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.wechat.config.WechatProperties; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class Code2SessionParam { + private String appid; + private String secret; + private String js_code; + private String access_token; + private String grant_type = "authorization_code"; + + public Code2SessionParam() { + WechatProperties wechatProperties = SpringUtil.getBean(WechatProperties.class); + this.appid = wechatProperties.getAppId(); + this.secret = wechatProperties.getAppSecret(); + } +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/GetAccessTokenParam.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/GetAccessTokenParam.java new file mode 100644 index 0000000..25bf45f --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/param/GetAccessTokenParam.java @@ -0,0 +1,24 @@ +package com.njzscloud.common.wechat.param; + +import cn.hutool.extra.spring.SpringUtil; +import com.njzscloud.common.wechat.config.WechatProperties; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class GetAccessTokenParam { + private String grant_type = "client_credential"; + private String appid; + private String secret; + + public GetAccessTokenParam() { + WechatProperties wechatProperties = SpringUtil.getBean(WechatProperties.class); + this.appid = wechatProperties.getAppId(); + this.secret = wechatProperties.getAppSecret(); + } +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/Code2SessionResult.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/Code2SessionResult.java new file mode 100644 index 0000000..cb2940a --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/Code2SessionResult.java @@ -0,0 +1,22 @@ +package com.njzscloud.common.wechat.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class Code2SessionResult { + private String session_key; + private String unionid; + private String openid; + private String errmsg; + private Integer errcode; + + public boolean isSucc() { + return errcode != null && errcode == 0; + } +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/GetAccessTokenResult.java b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/GetAccessTokenResult.java new file mode 100644 index 0000000..0836c36 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/java/com/njzscloud/common/wechat/result/GetAccessTokenResult.java @@ -0,0 +1,15 @@ +package com.njzscloud.common.wechat.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class GetAccessTokenResult { + private String access_token; + private Integer expires_in; +} diff --git a/njzscloud-common/njzscloud-common-wechat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-wechat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2076a88 --- /dev/null +++ b/njzscloud-common/njzscloud-common-wechat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.wechat.config.WechatAutoConfiguration diff --git a/njzscloud-common/njzscloud-common-ws/pom.xml b/njzscloud-common/njzscloud-common-ws/pom.xml new file mode 100644 index 0000000..ae641b0 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/pom.xml @@ -0,0 +1,38 @@ + + 4.0.0 + + com.njzscloud + njzscloud-common + 0.0.1 + + + njzscloud-common-ws + jar + + + UTF-8 + + + + + com.njzscloud + njzscloud-common-core + provided + + + com.njzscloud + njzscloud-common-security + provided + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.springframework + spring-websocket + + + diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java new file mode 100644 index 0000000..851a1ed --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java @@ -0,0 +1,65 @@ +package com.njzscloud.common.ws.config; + +import com.njzscloud.common.ws.support.ImpartialWebSocketHandler; +import com.njzscloud.common.ws.support.TokenHandshakeInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +import java.time.Duration; + +@Configuration +@EnableWebSocket +@RequiredArgsConstructor +@EnableConfigurationProperties(WebsocketProperties.class) +public class WebSocketAutoConfiguration implements WebSocketConfigurer { + + private final WebsocketProperties websocketProperties; + + // websocket-heartbeat-js + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + String path = websocketProperties.getPath(); + registry.addHandler(new ImpartialWebSocketHandler(), path) + .addInterceptors(new TokenHandshakeInterceptor()) + .setAllowedOrigins("*"); + } + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + Duration asyncSendTimeout = websocketProperties.getAsyncSendTimeout(); + Duration maxSessionIdleTimeout = websocketProperties.getMaxSessionIdleTimeout(); + DataSize maxTextMessageBufferSize = websocketProperties.getMaxTextMessageBufferSize(); + DataSize maxBinaryMessageBufferSize = websocketProperties.getMaxBinaryMessageBufferSize(); + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + + if (asyncSendTimeout != null) { + long seconds = asyncSendTimeout.getSeconds(); + container.setAsyncSendTimeout(seconds * 1000); + } + + if (maxSessionIdleTimeout != null) { + long seconds = maxSessionIdleTimeout.getSeconds(); + container.setMaxSessionIdleTimeout(seconds * 1000); + } + + if (maxTextMessageBufferSize != null) { + long size = maxTextMessageBufferSize.toBytes(); + container.setMaxTextMessageBufferSize((int) size); + } + + if (maxBinaryMessageBufferSize != null) { + long size = maxBinaryMessageBufferSize.toBytes(); + container.setMaxBinaryMessageBufferSize((int) size); + } + + return container; + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java new file mode 100644 index 0000000..3f2c0c1 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java @@ -0,0 +1,41 @@ +package com.njzscloud.common.ws.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.unit.DataSize; + +import java.time.Duration; + +/** + * websocket 配置 + */ +@Getter +@Setter +@ConfigurationProperties("ws") +public class WebsocketProperties { + /** + * websocket 请求地址, 默认 /fdx + */ + private String path = "/fdx"; + + /** + * 发送超时时间 + */ + private Duration asyncSendTimeout; + + /** + * 会话超时时间, 默认 15min + */ + private Duration maxSessionIdleTimeout = Duration.ofMinutes(15); + + /** + * 文本消息缓存大小, 默认 2MB + */ + private DataSize maxTextMessageBufferSize = DataSize.ofMegabytes(2); + + /** + * 二进制消息缓存大小, 默认 2MB + */ + private DataSize maxBinaryMessageBufferSize = DataSize.ofMegabytes(2); +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java new file mode 100644 index 0000000..4b26875 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java @@ -0,0 +1,17 @@ +package com.njzscloud.common.ws.support; + +/** + * 常量 + */ +public class Constants { + /** + * 客户端 Id 属性 + */ + public static final String SESSION_ATTRIBUTE_CID = "cid"; + + /** + * 用户 Id 属性 + */ + public static final String SESSION_ATTRIBUTE_UID = "uid"; + +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java new file mode 100644 index 0000000..4de5e9a --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java @@ -0,0 +1,99 @@ +package com.njzscloud.common.ws.support; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.core.ex.ExceptionMsg; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.AbstractWebSocketHandler; + + +/** + * websocket 处理器
+ * 不支持分片消息
+ * 支持文本、二进制信息 + */ +@Slf4j +public class ImpartialWebSocketHandler extends AbstractWebSocketHandler { + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + Wsdoor wsdoor = Wsdoor.of(session); + String cid = wsdoor.getCid(); + Long uid = wsdoor.getUid(); + wsdoor.send(new WsMsg() + .setEvent("connected") + .setData(R.success( + MapUtil.builder() + .put("uid", uid) + .put("cid", cid) + .build() + , "连接成功"))); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + Wsdoor wsdoor = Wsdoor.of(session); + WsMsg wsMsg = WsMsg.of(wsdoor, message); + UserDetail userDetail = SecurityUtil.loginUser(); + System.out.println(userDetail); + if (wsMsg == null + || StrUtil.isBlank(wsMsg.getAction()) + || StrUtil.isBlank(wsMsg.getEvent())) { + wsdoor.send(new WsMsg().setEvent("error") + .setData(R.failed(ExceptionMsg.CLI_ERR_MSG, "消息格式错误"))); + session.close(); + return; + } + String action = wsMsg.getAction(); + + if (!Websocket.ACTION_PUBLISH.equals(action) + && !Websocket.ACTION_PING.equals(action) + && !Websocket.ACTION_UNSUBSCRIBE.equals(action) + && !Websocket.ACTION_SUBSCRIBE.equals(action) + ) { + wsdoor.send(new WsMsg().setEvent("error") + .setData(R.failed(ExceptionMsg.CLI_ERR_MSG, "消息格式错误"))); + session.close(); + return; + } + + String event = wsMsg.getEvent(); + if (Websocket.ACTION_PUBLISH.equals(action)) { + Websocket.publish(wsMsg); + } else if (Websocket.ACTION_SUBSCRIBE.equals(action)) { + WsMsg.User from = wsMsg.getFrom(); + log.info("用户Id:{},客户端Id:{},订阅:{}", from.getUid(), from.getCid(), event); + Websocket.subscribe(event, wsdoor); + } else if (Websocket.ACTION_UNSUBSCRIBE.equals(action)) { + WsMsg.User from = wsMsg.getFrom(); + log.info("用户Id:{},客户端Id:{},取消订阅:{}", from.getUid(), from.getCid(), event); + Websocket.unsubscribe(event, wsdoor); + } else { + wsdoor.send(new WsMsg().setAction(Websocket.ACTION_PONG)); + } + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { + Wsdoor wsdoor = Wsdoor.of(session); + String cid = wsdoor.getCid(); + Long uid = wsdoor.getUid(); + log.error("连接失败: {} {}", cid, uid, exception); + Websocket.unsubscribe(wsdoor); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { + Wsdoor wsdoor = Wsdoor.of(session); + String cid = wsdoor.getCid(); + Long uid = wsdoor.getUid(); + log.info("连接关闭: {} {} {}", cid, uid, closeStatus); + Websocket.unsubscribe(wsdoor); + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java new file mode 100644 index 0000000..775d483 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java @@ -0,0 +1,58 @@ +package com.njzscloud.common.ws.support; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +// TODO TOKEN 处理器 + +/** + * websocket Token 拦截器
+ * 子协议格式: Auth [token],[cid], 如: Auth aimsn,niosdawq
+ * token: 用户登录凭证; cid: 客户端号, 推荐使用 token Id + */ +@Slf4j +public class TokenHandshakeInterceptor implements HandshakeInterceptor { + private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map attributes) { + try { + UserDetail userDetail = SecurityUtil.loginUser(); + Long uid = userDetail.getUserId(); + String cid = uid + "_" + IdUtil.fastSimpleUUID(); + attributes.put(Constants.SESSION_ATTRIBUTE_UID, uid); + attributes.put(Constants.SESSION_ATTRIBUTE_CID, cid); + return true; + } catch (Exception e) { + log.error("[Websocket 拦截器] 数据解析失败", e); + return false; + } + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + try { + HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest(); + HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse(); + String token = httpRequest.getHeader(SEC_WEB_SOCKET_PROTOCOL); + if (StrUtil.isNotEmpty(token)) { + httpResponse.addHeader(SEC_WEB_SOCKET_PROTOCOL, token); + } + } catch (ClassCastException e) { + log.error("[Websocket 拦截器] 类型转换失败: {}、{}", request.getClass(), response.getClass(), e); + } + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java new file mode 100644 index 0000000..86b5c3b --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java @@ -0,0 +1,10 @@ +package com.njzscloud.common.ws.support; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface WSListener { + String value(); +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java new file mode 100644 index 0000000..f9aa61d --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java @@ -0,0 +1,175 @@ +package com.njzscloud.common.ws.support; + +import cn.hutool.core.collection.CollUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * websocket 工具 + */ +@Slf4j +public class Websocket { + + public static final String ACTION_PUBLISH = "publish"; + public static final String ACTION_PING = "ping"; + public static final String ACTION_PONG = "pong"; + public static final String ACTION_SUBSCRIBE = "subscribe"; + public static final String ACTION_UNSUBSCRIBE = "unsubscribe"; + + /** + * 客户端事件订阅列表 + */ + static final ConcurrentHashMap> EVENT_CACHE = new ConcurrentHashMap<>(); + static final ReentrantReadWriteLock EVENT_CACHE_LOCK = new ReentrantReadWriteLock(); + static final ReentrantReadWriteLock.ReadLock EVENT_CACHE_READ_LOCK = EVENT_CACHE_LOCK.readLock(); + static final ReentrantReadWriteLock.WriteLock EVENT_CACHE_WRITE_LOCK = EVENT_CACHE_LOCK.writeLock(); + + static final ConcurrentHashMap>> LISTENER_CACHE = new ConcurrentHashMap<>(); + static final ReentrantReadWriteLock LISTENER_CACHE_LOCK = new ReentrantReadWriteLock(); + static final ReentrantReadWriteLock.ReadLock LISTENER_CACHE_READ_LOCK = LISTENER_CACHE_LOCK.readLock(); + static final ReentrantReadWriteLock.WriteLock LISTENER_CACHE_WRITE_LOCK = LISTENER_CACHE_LOCK.writeLock(); + + public static boolean publish(WsMsg wsMsg) { + wsMsg.setAction(ACTION_PUBLISH); + // 总数,成功数 + Integer[] success = {0, 0}; + String event = wsMsg.getEvent(); + List to = wsMsg.getTo(); + Set uids; + Set cids; + if (CollUtil.isNotEmpty(to)) { + uids = to.stream().map(WsMsg.User::getUid).collect(Collectors.toSet()); + cids = to.stream().map(WsMsg.User::getCid).collect(Collectors.toSet()); + } else { + uids = Collections.emptySet(); + cids = Collections.emptySet(); + } + EVENT_CACHE_READ_LOCK.lock(); + try { + EVENT_CACHE.computeIfPresent(event, (k, v) -> { + if (CollUtil.isEmpty(v)) return v; + List wsdoors = v; + if (!uids.isEmpty() || !cids.isEmpty()) { + wsdoors = v.stream().filter(it -> { + Long uid = it.getUid(); + String cid = it.getCid(); + return cids.contains(cid) || uids.contains(uid); + }).collect(Collectors.toList()); + } + success[1] = wsdoors.size(); + for (Wsdoor wsdoor : wsdoors) { + boolean send = wsdoor.send(wsMsg); + if (send) success[1] = success[1] + 1; + } + return v; + }); + } finally { + EVENT_CACHE_READ_LOCK.unlock(); + } + + LISTENER_CACHE_READ_LOCK.lock(); + try { + LISTENER_CACHE.computeIfPresent(event, (k, v) -> { + if (CollUtil.isEmpty(v)) return v; + success[1] = success[1] + v.size(); + for (Consumer listener : v) { + try { + listener.accept(wsMsg); + success[1] = success[1] + 1; + } catch (Exception e) { + log.error("事件 {} 监听方法执行异常", event, e); + } + } + return v; + }); + } finally { + LISTENER_CACHE_READ_LOCK.unlock(); + } + + return success[1] > 0; + } + + public static boolean publish(String event, Object data) { + return publish(new WsMsg() + .setAction(ACTION_PUBLISH) + .setEvent(event) + .setData(data)); + } + + public static void subscribe(String event, Wsdoor wsdoor) { + EVENT_CACHE_READ_LOCK.lock(); + try { + EVENT_CACHE.computeIfAbsent(event, k -> new LinkedList<>()); + + EVENT_CACHE.computeIfPresent(event, (k, v) -> { + v.add(wsdoor); + return v; + }); + } finally { + EVENT_CACHE_READ_LOCK.unlock(); + } + } + + public static void unsubscribe(Wsdoor wsdoor) { + EVENT_CACHE_WRITE_LOCK.lock(); + try { + EVENT_CACHE.forEach((k, v) -> { + v.removeIf(it -> it.equals(wsdoor)); + }); + EVENT_CACHE.entrySet().removeIf(it -> CollUtil.isEmpty(it.getValue())); + } finally { + EVENT_CACHE_WRITE_LOCK.unlock(); + } + } + + public static void unsubscribe(String event, Wsdoor wsdoor) { + EVENT_CACHE_WRITE_LOCK.lock(); + try { + EVENT_CACHE.computeIfPresent(event, (k, v) -> { + v.removeIf(it -> it.equals(wsdoor)); + return v; + }); + EVENT_CACHE.entrySet().removeIf(it -> CollUtil.isEmpty(it.getValue())); + } finally { + EVENT_CACHE_WRITE_LOCK.unlock(); + } + } + + /** + * 系统内部专用订阅, 用于系统事件通知
+ * + * @param event 事件名称 + */ + public static void subscribe(String event, Consumer listener) { + LISTENER_CACHE_READ_LOCK.lock(); + try { + LISTENER_CACHE.computeIfAbsent(event, k -> new LinkedList<>()); + LISTENER_CACHE.computeIfPresent(event, (k, v) -> { + v.add(listener); + return v; + }); + } finally { + LISTENER_CACHE_READ_LOCK.unlock(); + } + } + + public static void unsubscribe(String event, Consumer listener) { + LISTENER_CACHE_READ_LOCK.lock(); + try { + LISTENER_CACHE.computeIfPresent(event, (k, v) -> { + v.removeIf(it -> it == listener); + return v; + }); + } finally { + LISTENER_CACHE_READ_LOCK.unlock(); + } + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java new file mode 100644 index 0000000..622f704 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java @@ -0,0 +1,92 @@ +package com.njzscloud.common.ws.support; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.njzscloud.common.core.jackson.Jackson; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; + +import java.util.List; + +/** + * websocket 消息内容 + */ +@Slf4j +@Getter +@Setter +@Accessors(chain = true) +public class WsMsg { + /** + * 发送方 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private User from; + + /** + * 接收方 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private List to; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String action; + + /** + * 事件 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String event; + + /** + * 数据内容 + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private Object data; + + public static WsMsg of(Wsdoor wsdoor, TextMessage message) { + String payload = message.getPayload(); + try { + WsMsg wsMsg = Jackson.toBean(payload, WsMsg.class); + if (wsMsg != null) { + wsMsg.setFrom(new User() + .setUid(wsdoor.getUid()) + .setCid(wsdoor.getCid()) + ); + } + return wsMsg; + } catch (Exception e) { + log.error("WebSocket 消息反序列化失败, 数据: {}", payload, e); + return null; + } + } + + @Override + public String toString() { + return Jackson.toJsonStr(this); + } + + public T extractData(Class clazz) { + return Jackson.toBean(Jackson.toJsonStr(data), clazz); + } + + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class User { + /** + * 用户 Id + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + private Long uid; + /** + * 客户端 Id + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String cid; + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java new file mode 100644 index 0000000..07aaada --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java @@ -0,0 +1,73 @@ +package com.njzscloud.common.ws.support; + +import cn.hutool.core.map.MapUtil; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; +import java.util.Objects; + +@Slf4j +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class Wsdoor { + + /** + * 用户 Id + */ + private final Long uid; + /** + * 客户端 Id + */ + private final String cid; + /** + * 连接会话 + */ + private final WebSocketSession session; + + private Wsdoor(WebSocketSession session) { + Map attributes = session.getAttributes(); + this.uid = MapUtil.getLong(attributes, "uid"); + this.cid = MapUtil.getStr(attributes, "cid"); + this.session = session; + } + + public static Wsdoor of(WebSocketSession session) { + return new Wsdoor(session); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Wsdoor wsdoor = (Wsdoor) o; + return Objects.equals(uid, wsdoor.uid) && Objects.equals(cid, wsdoor.cid); + } + + @Override + public int hashCode() { + return Objects.hash(uid, cid); + } + + /** + * 发送文本数据 + * + * @param wsMsg 消息 + */ + public boolean send(WsMsg wsMsg) { + try { + if (!session.isOpen()) return false; + session.sendMessage(new TextMessage(wsMsg.toString())); + return true; + } catch (Exception e) { + log.error("消息发送失败", e); + } + return false; + } +} diff --git a/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..14fa9c4 --- /dev/null +++ b/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.njzscloud.common.ws.config.WebSocketAutoConfiguration diff --git a/njzscloud-common/pom.xml b/njzscloud-common/pom.xml new file mode 100644 index 0000000..6b5f164 --- /dev/null +++ b/njzscloud-common/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-dispose + 0.0.1 + + + njzscloud-common + + pom + + + njzscloud-common-core + njzscloud-common-security + njzscloud-common-mp + njzscloud-common-mvc + njzscloud-common-redis + njzscloud-common-oss + njzscloud-common-email + njzscloud-common-cache + njzscloud-common-sichen + njzscloud-common-sn + njzscloud-common-gen + njzscloud-common-mqtt + njzscloud-common-http + njzscloud-common-wechat + njzscloud-common-ws + njzscloud-common-sshtunnel + + + + diff --git a/njzscloud-svr/pom.xml b/njzscloud-svr/pom.xml new file mode 100644 index 0000000..ec55a18 --- /dev/null +++ b/njzscloud-svr/pom.xml @@ -0,0 +1,149 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-dispose + 0.0.1 + + njzscloud-svr + jar + + 21 + 21 + UTF-8 + greenfrog + + + + + com.njzscloud + njzscloud-common-core + + + com.njzscloud + njzscloud-common-http + + + com.njzscloud + njzscloud-common-sshtunnel + + + com.njzscloud + njzscloud-common-wechat + + + com.njzscloud + njzscloud-common-mp + + + com.njzscloud + njzscloud-common-mvc + + + com.njzscloud + njzscloud-common-sichen + + + com.njzscloud + njzscloud-common-sn + + + com.njzscloud + njzscloud-common-ws + + + + com.njzscloud + njzscloud-common-security + + + com.njzscloud + njzscloud-common-gen + + + com.njzscloud + njzscloud-common-oss + + + com.njzscloud + njzscloud-common-mqtt + + + jakarta.validation + jakarta.validation-api + + + + org.hibernate.validator + hibernate-validator + + + + + + ${appname} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + org.projectlombok + lombok + 1.18.30 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + post-package-actions + package + + run + + + + + + + + + + + + + + + + + + + + + + + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java new file mode 100644 index 0000000..2626efe --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/Main.java @@ -0,0 +1,11 @@ +package com.njzscloud.supervisory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppConfiguration.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppConfiguration.java new file mode 100644 index 0000000..a88de8c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppConfiguration.java @@ -0,0 +1,9 @@ +package com.njzscloud.supervisory.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({AppProperties.class}) +public class AppConfiguration { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppProperties.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppProperties.java new file mode 100644 index 0000000..1e7b950 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/AppProperties.java @@ -0,0 +1,15 @@ +package com.njzscloud.supervisory.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +@ConfigurationProperties(prefix = "app") +public class AppProperties { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/Example.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/Example.java new file mode 100644 index 0000000..b889726 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/Example.java @@ -0,0 +1,43 @@ +package com.njzscloud.supervisory.config; + +import cn.hutool.core.util.RandomUtil; +import com.njzscloud.common.sichen.support.Task; +import com.njzscloud.common.sichen.support.TaskHandle; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@org.springframework.stereotype.Component +public class Example { + + @Task(period = 6) + public void a() { + Thread thread = Thread.currentThread(); + boolean virtual = thread.isVirtual(); + String name = thread.getName(); + TaskHandle.debug("A-------当前时间:{} 线程名称:{} 是否虚拟线程:{}", System.currentTimeMillis() / 1000, name, virtual); + TaskHandle.info("A-------当前时间:{} 线程名称:{} 是否虚拟线程:{}", System.currentTimeMillis() / 1000, name, virtual); + TaskHandle.warn("A-------当前时间:{} 线程名称:{} 是否虚拟线程:{}", System.currentTimeMillis() / 1000, name, virtual); + TaskHandle.error("A-------当前时间:{} 线程名称:{} 是否虚拟线程:{}", System.currentTimeMillis() / 1000, name, virtual); + try { + int sleepTime = RandomUtil.randomInt(1000, 5000); + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + log.error("任务中断退出", e); + } + } + + /* @Task(period = 1) + public void b() { + log.info("B-------当前时间:{}", System.currentTimeMillis() / 1000); + } + + @Task(period = 1) + public void c() { + log.info("C-------当前时间:{}", System.currentTimeMillis() / 1000); + } + + @Task(cron = "0/1 * * * * ?") + public void d() { + log.info("D-------当前时间:{}", System.currentTimeMillis() / 1000); + } */ +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/SpringReady.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/SpringReady.java new file mode 100644 index 0000000..b393ac8 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/config/SpringReady.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.config; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class SpringReady { + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/controller/AuthController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/controller/AuthController.java new file mode 100644 index 0000000..b45ed36 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/controller/AuthController.java @@ -0,0 +1,32 @@ +package com.njzscloud.supervisory.sys.auth.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.SecurityUtil; +import com.njzscloud.supervisory.sys.auth.service.AuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录 + */ +@Slf4j +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + /** + * 获取当前登录人信息 + */ + @GetMapping("/my") + public R my() { + Long userId = SecurityUtil.currentUserId(); + + return R.success(authService.my(userId)); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/AuthMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/AuthMapper.java new file mode 100644 index 0000000..5347852 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/AuthMapper.java @@ -0,0 +1,23 @@ +package com.njzscloud.supervisory.sys.auth.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.njzscloud.supervisory.sys.auth.pojo.result.EndpointResource; +import com.njzscloud.supervisory.sys.auth.pojo.result.MenuResource; +import com.njzscloud.supervisory.sys.auth.pojo.result.MyResult; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +@Mapper +public interface AuthMapper { + + MyResult selectUser(@Param("ew") QueryWrapper ew); + + Set selectRole(@Param("userId") Long userId); + + List selectUserMenu(@Param("userId") Long userId); + + List selectUserEndpoint(@Param("userId") Long userId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/SysTokenMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/SysTokenMapper.java new file mode 100644 index 0000000..dc879b8 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/mapper/SysTokenMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.sys.auth.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.auth.pojo.entity.SysTokenEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 登录令牌表 + */ +@Mapper +public interface SysTokenMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/entity/SysTokenEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/entity/SysTokenEntity.java new file mode 100644 index 0000000..1ac0a4d --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/entity/SysTokenEntity.java @@ -0,0 +1,52 @@ +package com.njzscloud.supervisory.sys.auth.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 登录令牌表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_token") +public class SysTokenEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id + */ + private Long userId; + + /** + * Token Id + */ + private String tid; + + /** + * Token 键 + */ + private String tkey; + + /** + * Token 值 + */ + private String tval; + + /** + * 用户信息 + */ + private String userDetail; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/EndpointResource.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/EndpointResource.java new file mode 100644 index 0000000..34f4027 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/EndpointResource.java @@ -0,0 +1,47 @@ +package com.njzscloud.supervisory.sys.auth.pojo.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class EndpointResource { + + /** + * Id + */ + private Long id; + + /** + * 请求方式; 字典代码:request_method + */ + private String requestMethod; + + /** + * 路由前缀; 以 / 开头 或 为空 + */ + private String routingPath; + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; + + /** + * 接口访问模式; 字典代码:endpoint_access_model + */ + private String accessModel; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MenuResource.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MenuResource.java new file mode 100644 index 0000000..5f8a910 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MenuResource.java @@ -0,0 +1,73 @@ +package com.njzscloud.supervisory.sys.auth.pojo.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + + +@Getter +@Setter +@Accessors(chain = true) +public class MenuResource { + /** + * Id + */ + private Long id; + + /** + * 编号 + */ + private String sn; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 层级 + */ + private Integer tier; + + /** + * 面包路径; 逗号分隔 + */ + private List breadcrumb; + + /** + * 类型; 字典代码:menu_category + */ + private String menuCategory; + + /** + * 标签是否冻结; 0-->否、1-->是 + */ + private Boolean freeze; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + /** + * 路由路径 + */ + private String path; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MyResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MyResult.java new file mode 100644 index 0000000..866c1bb --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/pojo/result/MyResult.java @@ -0,0 +1,40 @@ +package com.njzscloud.supervisory.sys.auth.pojo.result; + +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.supervisory.sys.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@Accessors(chain = true) +public class MyResult extends UserDetail { + private List menus; + + private List endpoints; + + private List> setting; + + /** + * 头像 + */ + private String avatar; + /** + * 性别; 字典代码:gender + */ + private Gender gender; + /** + * 手机号 + */ + private String phone; + /** + * 邮箱 + */ + private String email; + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/AuthService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/AuthService.java new file mode 100644 index 0000000..0d59e14 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/AuthService.java @@ -0,0 +1,70 @@ +package com.njzscloud.supervisory.sys.auth.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.security.module.password.PasswordLoginForm; +import com.njzscloud.common.security.module.wechat.mini.WechatMiniLoginForm; +import com.njzscloud.common.security.support.IAuthService; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.supervisory.sys.auth.mapper.AuthMapper; +import com.njzscloud.supervisory.sys.auth.pojo.result.EndpointResource; +import com.njzscloud.supervisory.sys.auth.pojo.result.MenuResource; +import com.njzscloud.supervisory.sys.auth.pojo.result.MyResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; + +import static com.njzscloud.common.security.contant.Constants.ROLE_ANONYMOUS; +import static com.njzscloud.common.security.contant.Constants.ROLE_AUTHENTICATED; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService implements IAuthService { + private final AuthMapper authMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public UserDetail selectUser(PasswordLoginForm form) { + return authMapper.selectUser(Wrappers.query().eq("a.username", form.getAccount()).eq("a.deleted", 0)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public UserDetail selectUser(WechatMiniLoginForm form) { + String openid = form.getOpenid(); + String unionid = form.getUnionid(); + return authMapper.selectUser(Wrappers.query().eq("a.wechat_openid", openid) + .eq(StrUtil.isNotBlank(unionid), "a.wechat_unionid", unionid) + .eq("a.deleted", 0)); + } + + @Override + public Set selectRole(Long userId) { + return authMapper.selectRole(userId); + } + + /** + * 获取当前登录人信息 + */ + @Override + public UserDetail my(Long userId) { + UserDetail userDetail = authMapper.selectUser(Wrappers.query().eq("a.id", userId).eq("a.deleted", 0)); + List menuResources = authMapper.selectUserMenu(userId); + List endpointResources = authMapper.selectUserEndpoint(userId); + Set roles = authMapper.selectRole(userId); + roles.add(ROLE_AUTHENTICATED); + roles.add(ROLE_ANONYMOUS); + return BeanUtil.copyProperties(userDetail, MyResult.class) + .setMenus(menuResources) + .setEndpoints(endpointResources) + .setRoles(roles) + ; + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/TokenService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/TokenService.java new file mode 100644 index 0000000..935e787 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/auth/service/TokenService.java @@ -0,0 +1,116 @@ +package com.njzscloud.supervisory.sys.auth.service; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.fastjson.Fastjson; +import com.njzscloud.common.security.contant.Constants; +import com.njzscloud.common.security.support.ITokenService; +import com.njzscloud.common.security.support.Token; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.supervisory.sys.auth.mapper.SysTokenMapper; +import com.njzscloud.supervisory.sys.auth.pojo.entity.SysTokenEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Service +@RequiredArgsConstructor +public class TokenService implements ITokenService { + private static final TimedCache TOKEN_CACHE = CacheUtil.newTimedCache(60 * 1000); + private final SysTokenMapper sysTokenMapper; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + + @Override + public void saveToken(UserDetail userDetail) { + try { + writeLock.lock(); + Token token = userDetail.getToken(); + long userId = token.userId(); + String tid = token.tid(); + String key = Constants.TOKEN_CACHE_KEY.fill(userId, tid); + Long l = sysTokenMapper.selectCount(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + .eq(SysTokenEntity::getTid, tid)); + if (l > 0) return; + sysTokenMapper.insert(new SysTokenEntity() + .setUserId(userId) + .setTid(tid) + .setTkey(key) + .setTval(token.toString()) + .setUserDetail(Fastjson.toJsonStr(userDetail)) + ); + TOKEN_CACHE.put(key, userDetail); + } finally { + writeLock.unlock(); + } + } + + @Override + public UserDetail loadUser(String tokenStr) { + try { + readLock.lock(); + Token token = Token.of(tokenStr); + String tid = token.tid(); + long userId = token.userId(); + String key = Constants.TOKEN_CACHE_KEY.fill(userId, tid); + UserDetail userDetail = TOKEN_CACHE.get(key); + if (userDetail != null) return userDetail; + SysTokenEntity sysTokenEntity = sysTokenMapper.selectOne(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + .eq(SysTokenEntity::getTid, tid)); + if (sysTokenEntity == null) return null; + userDetail = Fastjson.toBean(sysTokenEntity.getUserDetail(), + UserDetail.class); + TOKEN_CACHE.put(key, userDetail); + return userDetail; + } finally { + readLock.unlock(); + } + } + + @Override + public void removeToken(Token token) { + try { + writeLock.lock(); + long userId = token.userId(); + String tid = token.tid(); + String key = Constants.TOKEN_CACHE_KEY.fill(userId, tid); + TOKEN_CACHE.remove(key); + sysTokenMapper.delete(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + .eq(SysTokenEntity::getTid, tid) + ); + } finally { + writeLock.unlock(); + } + } + + @Override + public void removeToken(Long userId) { + try { + writeLock.lock(); + ArrayList keys = new ArrayList<>(); + for (UserDetail userDetail : TOKEN_CACHE) { + Token token = userDetail.getToken(); + if (token.userId() == userId) { + String key = Constants.TOKEN_CACHE_KEY.fill(userId, token.tid()); + keys.add(key); + } + } + for (String key : keys) { + TOKEN_CACHE.remove(key); + } + sysTokenMapper.delete(Wrappers.lambdaQuery() + .eq(SysTokenEntity::getUserId, userId) + ); + } finally { + writeLock.unlock(); + } + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/DictController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/DictController.java new file mode 100644 index 0000000..7541b35 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/DictController.java @@ -0,0 +1,80 @@ +package com.njzscloud.supervisory.sys.dict.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.dict.pojo.DictEntity; +import com.njzscloud.supervisory.sys.dict.pojo.ObtainDictDataResult; +import com.njzscloud.supervisory.sys.dict.service.DictService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 字典表 + */ +@Slf4j +@RestController +@RequestMapping("/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody DictEntity dictEntity) { + dictService.add(dictEntity); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody DictEntity dictEntity) { + dictService.modify(dictEntity); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + dictService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(dictService.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, DictEntity dictEntity) { + return R.success(dictService.paging(pageParam, dictEntity)); + } + + + /** + * 获取字典数据 + */ + @GetMapping("/dict_data") + public R> obtainDictData(@RequestParam("dictKey") String dictKey, + @RequestParam(value = "txt", required = false) String txt) { + return R.success(dictService.obtainDictData(dictKey, txt)); + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/SysDictItemController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/SysDictItemController.java new file mode 100644 index 0000000..886bad5 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/controller/SysDictItemController.java @@ -0,0 +1,68 @@ +package com.njzscloud.supervisory.sys.dict.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.dict.pojo.DictItemEntity; +import com.njzscloud.supervisory.sys.dict.service.DictItemService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 字典条目表 + */ +@Slf4j +@RestController +@RequestMapping("/dict_item") +@RequiredArgsConstructor +public class SysDictItemController { + + private final DictItemService dictItemService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody DictItemEntity dictItemEntity) { + dictItemService.add(dictItemEntity); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody DictItemEntity dictItemEntity) { + dictItemService.modify(dictItemEntity); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + dictItemService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(dictItemService.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, DictItemEntity dictItemEntity) { + return R.success(dictItemService.paging(pageParam, dictItemEntity)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictItemMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictItemMapper.java new file mode 100644 index 0000000..5fa7fe7 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictItemMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.sys.dict.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.dict.pojo.DictItemEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典条目表 + */ +@Mapper +public interface DictItemMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictMapper.java new file mode 100644 index 0000000..3d2dfeb --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/mapper/DictMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.sys.dict.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.dict.pojo.DictEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典表 + */ +@Mapper +public interface DictMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictEntity.java new file mode 100644 index 0000000..7c03589 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictEntity.java @@ -0,0 +1,47 @@ +package com.njzscloud.supervisory.sys.dict.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 字典表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_dict") +public class DictEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 字典标识 + */ + private String dictKey; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictItemEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictItemEntity.java new file mode 100644 index 0000000..d3e4dd9 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/DictItemEntity.java @@ -0,0 +1,62 @@ +package com.njzscloud.supervisory.sys.dict.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 字典条目表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_dict_item") +public class DictItemEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 字典 Id; sys_dict.id + */ + private Long dictId; + + /** + * 字典标识; sys_dict.dict_key + */ + private String dictKey; + + /** + * 值; 分类值/字典项值 + */ + private String val; + + /** + * 显示文本; 分类显示文本/字典项显示文本 + */ + private String txt; + + /** + * 排序 + */ + private Integer sort; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/ObtainDictDataResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/ObtainDictDataResult.java new file mode 100644 index 0000000..dd2d273 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/pojo/ObtainDictDataResult.java @@ -0,0 +1,57 @@ +package com.njzscloud.supervisory.sys.dict.pojo; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 字典条目表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class ObtainDictDataResult { + + /** + * Id + */ + private Long id; + + /** + * 字典 Id; sys_dict.id + */ + private Long dictId; + + /** + * 字典标识; sys_dict.dict_key + */ + private String dictKey; + + /** + * 值; 分类值/字典项值 + */ + private String val; + + /** + * 显示文本; 分类显示文本/字典项显示文本 + */ + private String txt; + + /** + * 排序 + */ + private Integer sort; + + /** + * 备注 + */ + private String memo; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictItemService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictItemService.java new file mode 100644 index 0000000..150c0dc --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictItemService.java @@ -0,0 +1,110 @@ +package com.njzscloud.supervisory.sys.dict.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.dict.mapper.DictItemMapper; +import com.njzscloud.supervisory.sys.dict.pojo.DictItemEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 字典条目表 + */ +@Slf4j +@Service +public class DictItemService extends ServiceImpl implements IService { + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public void add(DictItemEntity dictItemEntity) { + Long dictId = dictItemEntity.getDictId(); + String val = dictItemEntity.getVal(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(DictItemEntity::getDictId, dictId).eq(DictItemEntity::getVal, val)); + Assert.isFalse(exists, () -> Exceptions.clierr("字典项已存在:{}", val)); + Integer sort = dictItemEntity.getSort(); + if (sort == null || sort == 0) { + Integer maxSort = this.getOne(Wrappers.query().eq("dict_id", dictId).orderByDesc("sort").select("IFNULL(MAX(sort),0) sort")).getSort(); + dictItemEntity.setSort(maxSort + 1); + } else { + this.update(Wrappers.lambdaUpdate().eq(DictItemEntity::getDictId, dictId).ge(DictItemEntity::getSort, sort).setIncrBy(DictItemEntity::getSort, 1)); + } + + this.save(dictItemEntity); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(DictItemEntity dictItemEntity) { + Long id = dictItemEntity.getId(); + DictItemEntity oldData = this.getById(id); + Assert.notNull(oldData, () -> Exceptions.clierr("为查询到要修改的数据")); + + Long dictId = oldData.getDictId(); + String val = dictItemEntity.getVal(); + if (StrUtil.isNotBlank(val)) { + boolean exists = this.exists(Wrappers.lambdaQuery().eq(DictItemEntity::getDictId, dictId).eq(DictItemEntity::getVal, val).ne(DictItemEntity::getId, id)); + Assert.isFalse(exists, () -> Exceptions.clierr("字典项已存在:{}", val)); + } + + Integer sort = dictItemEntity.getSort(); + if (sort != null && sort != 0) { + List list = this.list(Wrappers.lambdaQuery() + .eq(DictItemEntity::getDictId, dictId) + .ge(DictItemEntity::getSort, sort) + .ne(DictItemEntity::getId, id) + .select(DictItemEntity::getSort, DictItemEntity::getId) + ); + if (CollUtil.isNotEmpty(list)) { + list.forEach(item -> item.setSort(sort + 1)); + this.updateBatchById(list); + } + } + + this.updateById(dictItemEntity); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public DictItemEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, DictItemEntity dictItemEntity) { + Long dictId = dictItemEntity.getDictId(); + String val = dictItemEntity.getVal(); + String txt = dictItemEntity.getTxt(); + return PageResult.of(this.page(pageParam.toPage(), Wrappers.lambdaQuery() + .eq(DictItemEntity::getDictId, dictId) + .like(StrUtil.isNotBlank(val), DictItemEntity::getVal, val) + .like(StrUtil.isNotBlank(txt), DictItemEntity::getTxt, txt) + .orderByAsc(DictItemEntity::getSort) + )); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictService.java new file mode 100644 index 0000000..0f2ed49 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/dict/service/DictService.java @@ -0,0 +1,93 @@ +package com.njzscloud.supervisory.sys.dict.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.dict.mapper.DictMapper; +import com.njzscloud.supervisory.sys.dict.pojo.DictEntity; +import com.njzscloud.supervisory.sys.dict.pojo.DictItemEntity; +import com.njzscloud.supervisory.sys.dict.pojo.ObtainDictDataResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 字典表 + */ +@Slf4j +@Service +public class DictService extends ServiceImpl implements IService { + @Autowired + private DictItemService dictItemService; + + /** + * 新增 + */ + public void add(DictEntity dictEntity) { + String dictKey = dictEntity.getDictKey(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(DictEntity::getDictKey, dictKey)); + Assert.isFalse(exists, () -> Exceptions.clierr("字典标识重复:{}", dictKey)); + this.save(dictEntity); + } + + /** + * 修改 + */ + public void modify(DictEntity dictEntity) { + this.updateById(dictEntity); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + dictItemService.remove(Wrappers.lambdaQuery().in(DictItemEntity::getDictId, ids)); + } + + /** + * 详情 + */ + public DictEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, DictEntity dictEntity) { + String dictName = dictEntity.getDictName(); + return PageResult.of(this.page(pageParam.toPage(), Wrappers.lambdaQuery() + .and(StrUtil.isNotBlank(dictName), it -> + it.like(DictEntity::getDictKey, dictName) + .or().like(DictEntity::getDictName, dictName) + .or().like(DictEntity::getMemo, dictName) + ) + )); + } + + public List obtainDictData(String dictKey, String keywords) { + return dictItemService.list(Wrappers.lambdaQuery().eq(DictItemEntity::getDictKey, dictKey) + .and(StrUtil.isNotBlank(keywords), it -> + it.like(DictItemEntity::getTxt, keywords) + .or().like(DictItemEntity::getVal, keywords) + .or().like(DictItemEntity::getMemo, keywords) + ) + .orderByAsc(DictItemEntity::getSort, DictItemEntity::getVal) + ) + .stream().map(it -> BeanUtil.copyProperties(it, ObtainDictDataResult.class)) + .collect(Collectors.toList()); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/controller/DistrictController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/controller/DistrictController.java new file mode 100644 index 0000000..9877f51 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/controller/DistrictController.java @@ -0,0 +1,76 @@ +package com.njzscloud.supervisory.sys.district.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.district.pojo.entity.DistrictEntity; +import com.njzscloud.supervisory.sys.district.pojo.param.SearchDistrictParam; +import com.njzscloud.supervisory.sys.district.service.DistrictService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 省市区表 + */ +@Slf4j +@RestController +@RequestMapping("/district") +@RequiredArgsConstructor +public class DistrictController { + private final DistrictService districtService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody DistrictEntity districtEntity) { + districtService.add(districtEntity); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody DistrictEntity districtEntity) { + districtService.modify(districtEntity); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + districtService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(districtService.detail(id)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, SearchDistrictParam searchDistrictParam) { + return R.success(districtService.paging(pageParam, searchDistrictParam)); + } + + /** + * 查询所有 + */ + @GetMapping("/list") + public R> list(SearchDistrictParam searchDistrictParam) { + return R.success(districtService.list(searchDistrictParam)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/mapper/DistrictMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/mapper/DistrictMapper.java new file mode 100644 index 0000000..9a8f93b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/mapper/DistrictMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.supervisory.sys.district.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.district.pojo.entity.DistrictEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 省市区表 + */ +@Mapper +public interface DistrictMapper extends BaseMapper { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/entity/DistrictEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/entity/DistrictEntity.java new file mode 100644 index 0000000..e13738a --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/entity/DistrictEntity.java @@ -0,0 +1,67 @@ +package com.njzscloud.supervisory.sys.district.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 省市区表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_district") +public class DistrictEntity { + + /** + * Id; 地区代码 + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 上级地区代码 + */ + private String pid; + + /** + * 省 + */ + private String province; + + /** + * 市 + */ + private String city; + + /** + * 区县 + */ + private String area; + + /** + * 乡镇街道 + */ + private String town; + + /** + * 层级; >= 1 + */ + private Integer tier; + + /** + * 地区名称 + */ + private String districtName; + + /** + * 排序 + */ + private Integer sort; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/param/SearchDistrictParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/param/SearchDistrictParam.java new file mode 100644 index 0000000..3f37d75 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/param/SearchDistrictParam.java @@ -0,0 +1,25 @@ +package com.njzscloud.supervisory.sys.district.pojo.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 省市区表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class SearchDistrictParam { + /** + * 上级地区代码 + */ + private String pid; + + /** + * 地区名称 + */ + private String districtName; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/result/DistrictTreeResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/result/DistrictTreeResult.java new file mode 100644 index 0000000..beeca80 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/pojo/result/DistrictTreeResult.java @@ -0,0 +1,61 @@ +package com.njzscloud.supervisory.sys.district.pojo.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +public class DistrictTreeResult { + + /** + * Id; 地区代码 + */ + private String id; + + /** + * 上级地区代码 + */ + private String pid; + + /** + * 省 + */ + private String province; + + /** + * 市 + */ + private String city; + + /** + * 区县 + */ + private String area; + + /** + * 乡镇街道 + */ + private String town; + + /** + * 层级; >= 1 + */ + private Integer tier; + + /** + * 地区名称 + */ + private String districtName; + + /** + * 排序 + */ + + private Integer sort; + + private List children; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/service/DistrictService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/service/DistrictService.java new file mode 100644 index 0000000..c1bd984 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/district/service/DistrictService.java @@ -0,0 +1,77 @@ +package com.njzscloud.supervisory.sys.district.service; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.district.mapper.DistrictMapper; +import com.njzscloud.supervisory.sys.district.pojo.entity.DistrictEntity; +import com.njzscloud.supervisory.sys.district.pojo.param.SearchDistrictParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 省市区表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DistrictService extends ServiceImpl implements IService { + + /** + * 新增 + */ + public void add(DistrictEntity districtEntity) { + this.save(districtEntity); + } + + /** + * 修改 + */ + public void modify(DistrictEntity districtEntity) { + this.updateById(districtEntity); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public DistrictEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, SearchDistrictParam searchDistrictParam) { + String pid = searchDistrictParam.getPid(); + String districtName = searchDistrictParam.getDistrictName(); + return PageResult.of(this.page(pageParam.toPage(), Wrappers.lambdaQuery() + .eq(DistrictEntity::getPid, pid) + .like(StrUtil.isNotBlank(districtName), DistrictEntity::getDistrictName, districtName))); + } + + /** + * 查询所有 + */ + public List list(SearchDistrictParam searchDistrictParam) { + String pid = searchDistrictParam.getPid(); + String districtName = searchDistrictParam.getDistrictName(); + return this.list(Wrappers.lambdaQuery() + .eq(DistrictEntity::getPid, pid) + .like(StrUtil.isNotBlank(districtName), DistrictEntity::getDistrictName, districtName)); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/contant/RequestMethod.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/contant/RequestMethod.java new file mode 100644 index 0000000..438b8d1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/contant/RequestMethod.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.sys.endpoint.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:request_method + * 字典名称:HTTP 请求方式 + */ +@Getter +@RequiredArgsConstructor +public enum RequestMethod implements DictStr { + GET("GET", "GET 请求"), + POST("POST", "POST 请求"), + PUT("PUT", "PUT 请求"), + DELETE("DELETE", "DELETE 请求"); + + private final String val; + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/controller/EndpointController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/controller/EndpointController.java new file mode 100644 index 0000000..e339474 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/controller/EndpointController.java @@ -0,0 +1,68 @@ +package com.njzscloud.supervisory.sys.endpoint.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.supervisory.sys.endpoint.pojo.entity.EndpointEntity; +import com.njzscloud.supervisory.sys.endpoint.pojo.param.EndpointSearchParam; +import com.njzscloud.supervisory.sys.endpoint.pojo.result.EndpointDetailResult; +import com.njzscloud.supervisory.sys.endpoint.service.EndpointService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 端点信息表 + */ +@Slf4j +@RestController +@RequestMapping("/endpoint") +@RequiredArgsConstructor +public class EndpointController { + + private final EndpointService endpointService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody EndpointEntity endpointEntity) { + endpointService.add(endpointEntity); + return R.success(); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody EndpointEntity endpointEntity) { + endpointService.modify(endpointEntity); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + endpointService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(endpointService.detail(id)); + } + + /** + * 查询 + */ + @GetMapping("/list_all") + public R> listAll(EndpointSearchParam endpointSearchParam) { + return R.success(endpointService.listAll(endpointSearchParam)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/mapper/EndpointMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/mapper/EndpointMapper.java new file mode 100644 index 0000000..71eb419 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/mapper/EndpointMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.supervisory.sys.endpoint.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.endpoint.pojo.entity.EndpointEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 端点信息表 + */ +@Mapper +public interface EndpointMapper extends BaseMapper { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/entity/EndpointEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/entity/EndpointEntity.java new file mode 100644 index 0000000..589de11 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/entity/EndpointEntity.java @@ -0,0 +1,54 @@ +package com.njzscloud.supervisory.sys.endpoint.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njzscloud.common.security.contant.EndpointAccessModel; +import com.njzscloud.supervisory.sys.endpoint.contant.RequestMethod; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_endpoint") +public class EndpointEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 请求方式; 字典代码:request_method + */ + private RequestMethod requestMethod; + + /** + * 路由前缀; 以 / 开头 或 为空 + */ + private String routingPath; + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; + + /** + * 接口访问模式; 字典代码:endpoint_access_model + */ + private EndpointAccessModel accessModel; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/param/EndpointSearchParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/param/EndpointSearchParam.java new file mode 100644 index 0000000..b28d523 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/param/EndpointSearchParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.sys.endpoint.pojo.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class EndpointSearchParam { + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/result/EndpointDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/result/EndpointDetailResult.java new file mode 100644 index 0000000..cfd6902 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/pojo/result/EndpointDetailResult.java @@ -0,0 +1,49 @@ +package com.njzscloud.supervisory.sys.endpoint.pojo.result; + +import com.njzscloud.common.security.contant.EndpointAccessModel; +import com.njzscloud.supervisory.sys.endpoint.contant.RequestMethod; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 端点信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class EndpointDetailResult { + + /** + * Id + */ + private Long id; + + /** + * 请求方式; 字典代码:request_method + */ + private RequestMethod requestMethod; + + /** + * 路由前缀; 以 / 开头 或 为空 + */ + private String routingPath; + + /** + * 端点地址; 以 / 开头, Ant 匹配模式 + */ + private String endpointPath; + + /** + * 接口访问模式; 字典代码:endpoint_access_model + */ + private EndpointAccessModel accessModel; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/service/EndpointService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/service/EndpointService.java new file mode 100644 index 0000000..4ca0919 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/endpoint/service/EndpointService.java @@ -0,0 +1,67 @@ +package com.njzscloud.supervisory.sys.endpoint.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.supervisory.sys.endpoint.mapper.EndpointMapper; +import com.njzscloud.supervisory.sys.endpoint.pojo.entity.EndpointEntity; +import com.njzscloud.supervisory.sys.endpoint.pojo.param.EndpointSearchParam; +import com.njzscloud.supervisory.sys.endpoint.pojo.result.EndpointDetailResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 端点信息表 + */ +@Slf4j +@Service +public class EndpointService extends ServiceImpl implements IService { + + /** + * 新增 + */ + public void add(EndpointEntity endpointEntity) { + this.save(endpointEntity); + } + + /** + * 修改 + */ + public void modify(EndpointEntity endpointEntity) { + this.updateById(endpointEntity); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + */ + public EndpointEntity detail(Long id) { + return this.getById(id); + } + + /** + * 查询所有 + */ + public List listAll(EndpointSearchParam endpointSearchParam) { + return this.list(Wrappers.lambdaQuery() + .like(StrUtil.isNotBlank(endpointSearchParam.getEndpointPath()), EndpointEntity::getEndpointPath, endpointSearchParam.getEndpointPath()) + .orderByAsc(EndpointEntity::getRequestMethod, EndpointEntity::getRoutingPath, EndpointEntity::getEndpointPath) + ) + .stream() + .map(it -> BeanUtil.copyProperties(it, EndpointDetailResult.class)) + .collect(Collectors.toList()); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/contant/MenuCategory.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/contant/MenuCategory.java new file mode 100644 index 0000000..6aa6d94 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/contant/MenuCategory.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.sys.menu.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 菜单类型 + */ +@Getter +@RequiredArgsConstructor +public enum MenuCategory implements DictStr { + Catalog("Catalog", "目录"), + Group("Group", "组"), + Page("Page", "页面"), + SubPage("SubPage", "子页面"), + Btn("Btn", "按钮"); + + private final String val; + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/controller/MenuController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/controller/MenuController.java new file mode 100644 index 0000000..2767482 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/controller/MenuController.java @@ -0,0 +1,83 @@ +package com.njzscloud.supervisory.sys.menu.controller; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuAddParam; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuModifyParam; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuSearchParam; +import com.njzscloud.supervisory.sys.menu.pojo.result.MenuDetailResult; +import com.njzscloud.supervisory.sys.menu.service.MenuService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 菜单 + */ +@Slf4j +@RestController +@RequestMapping("/menu") +@RequiredArgsConstructor +public class MenuController { + + private final MenuService menuService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody @Validated MenuAddParam menuAddParam) { + return R.success(menuService.add(menuAddParam)); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody @Validated MenuModifyParam menuModifyParam) { + menuService.modify(menuModifyParam); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, "未指定要删除的数据"); + menuService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam Long id) { + return R.success(menuService.detail(id)); + } + + /** + * 查询所有 + */ + @GetMapping("/list_all") + public R> listAll(MenuSearchParam menuSearchParam) { + return R.success(menuService.listAll(menuSearchParam)); + } + + + /** + * 分页查询 + */ + @GetMapping("/page_list") + public R> paging(PageParam pageParam, MenuSearchParam menuSearchParam) { + return R.success(menuService.paging(pageParam, menuSearchParam)); + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/mapper/MenuMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/mapper/MenuMapper.java new file mode 100644 index 0000000..b37a2d2 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/mapper/MenuMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.supervisory.sys.menu.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.menu.pojo.entity.MenuEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 菜单信息表 + */ +@Mapper +public interface MenuMapper extends BaseMapper { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/entity/MenuEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/entity/MenuEntity.java new file mode 100644 index 0000000..f259b20 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/entity/MenuEntity.java @@ -0,0 +1,100 @@ +package com.njzscloud.supervisory.sys.menu.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.njzscloud.common.mp.support.handler.j.JsonTypeHandler; +import com.njzscloud.supervisory.sys.menu.contant.MenuCategory; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 菜单信息表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName(value = "sys_menu", autoResultMap = true) +public class MenuEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String sn; + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 层级; >= 1 + */ + private Integer tier; + + /** + * 面包路径; 逗号分隔 + */ + @TableField(value = "breadcrumb", typeHandler = JsonTypeHandler.class) + private List breadcrumb; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuAddParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuAddParam.java new file mode 100644 index 0000000..2454169 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuAddParam.java @@ -0,0 +1,73 @@ +package com.njzscloud.supervisory.sys.menu.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.supervisory.sys.menu.contant.MenuCategory; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +public class MenuAddParam implements Constrained { + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + @NotBlank(message = "菜单名称不能为空") + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + private String sn; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> pid == null || pid >= 0, "上级 Id 必须大于 0 或不指定"), + ValidRule.of(() -> menuCategory != null, "类型不能为空"), + ValidRule.of(() -> { + if ((menuCategory == MenuCategory.Catalog || menuCategory == MenuCategory.Btn)) { + return StrUtil.isBlank(routeName); + } else { + return true; + } + }, "菜单目录和按钮不用指定路由名称"), + ValidRule.of(() -> { + if (menuCategory == MenuCategory.Page || menuCategory == MenuCategory.SubPage) { + return StrUtil.isNotBlank(routeName); + } else { + return true; + } + }, "路由名称不能为空"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuModifyParam.java new file mode 100644 index 0000000..f67f88e --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuModifyParam.java @@ -0,0 +1,56 @@ +package com.njzscloud.supervisory.sys.menu.pojo.param; + +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class MenuModifyParam implements Constrained { + + /** + * Id + */ + private Long id; + + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> id != null, "未知定要修改的菜单"), + ValidRule.of(() -> pid == null || pid >= 0, "上级 Id 必须大于 0 或不指定"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuSearchParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuSearchParam.java new file mode 100644 index 0000000..2d36842 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/param/MenuSearchParam.java @@ -0,0 +1,30 @@ +package com.njzscloud.supervisory.sys.menu.pojo.param; + +import com.njzscloud.common.mvc.validator.Constraint; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 添加菜单 + */ +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class MenuSearchParam { + + private Long pid; + + + /** + * 菜单名称 + */ + private String title; + + + /** + * 路由名称 + */ + private String routeName; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/result/MenuDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/result/MenuDetailResult.java new file mode 100644 index 0000000..5c0f743 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/pojo/result/MenuDetailResult.java @@ -0,0 +1,57 @@ +package com.njzscloud.supervisory.sys.menu.pojo.result; + +import com.njzscloud.supervisory.sys.menu.contant.MenuCategory; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 添加菜单 + */ +@Getter +@Setter +@EqualsAndHashCode +@Accessors(chain = true) +public class MenuDetailResult { + + /** + * 主键 Id + */ + private Long id; + private String sn; + /** + * 上级 Id; 层级为 1 的节点值为 0 + */ + private Long pid; + private Integer tier; + + /** + * 菜单名称 + */ + private String title; + + /** + * 图标 + */ + private String icon; + + /** + * 类型; 字典代码:menu_category + */ + private MenuCategory menuCategory; + + private List breadcrumb; + + /** + * 排序 + */ + private Integer sort; + + /** + * 路由名称 + */ + private String routeName; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/service/MenuService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/service/MenuService.java new file mode 100644 index 0000000..ab278e1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/menu/service/MenuService.java @@ -0,0 +1,222 @@ +package com.njzscloud.supervisory.sys.menu.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.menu.contant.MenuCategory; +import com.njzscloud.supervisory.sys.menu.mapper.MenuMapper; +import com.njzscloud.supervisory.sys.menu.pojo.entity.MenuEntity; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuAddParam; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuModifyParam; +import com.njzscloud.supervisory.sys.menu.pojo.param.MenuSearchParam; +import com.njzscloud.supervisory.sys.menu.pojo.result.MenuDetailResult; +import com.njzscloud.supervisory.sys.resource.contant.ResourceOrigin; +import com.njzscloud.supervisory.sys.resource.pojo.entity.ResourceEntity; +import com.njzscloud.supervisory.sys.resource.service.ResourceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 菜单信息表 + */ +@Slf4j +@Service +public class MenuService extends ServiceImpl implements IService { + + @Autowired + private ResourceService resourceService; + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public Long add(MenuAddParam menuAddParam) { + String sn = menuAddParam.getSn(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(MenuEntity::getSn, sn)); + Assert.isFalse(exists, () -> Exceptions.exception("资源编码重复")); + MenuEntity menuEntity = BeanUtil.copyProperties(menuAddParam, MenuEntity.class); + Long pid = menuEntity.getPid(); + if (pid == null) { + menuEntity.setPid(0L); + pid = 0L; + } + String title = menuEntity.getTitle(); + if (pid == 0) { + menuEntity.setTier(1); + menuEntity.setBreadcrumb(CollUtil.newArrayList(title)); + } else { + MenuEntity parent = this.getById(pid); + Assert.notNull(parent, () -> Exceptions.exception("上级菜单不存在")); + menuEntity.setTier(parent.getTier() + 1); + List breadcrumb = parent.getBreadcrumb(); + breadcrumb.add(title); + menuEntity.setBreadcrumb(breadcrumb); + } + + Integer sort = menuEntity.getSort(); + if (sort == null || sort == 0) { + Integer maxSort = this.getOne(Wrappers.query().eq("pid", pid).orderByDesc("sort").select("IFNULL(MAX(sort),0) sort")).getSort(); + menuEntity.setSort(maxSort + 1); + } else { + this.update(Wrappers.lambdaUpdate().eq(MenuEntity::getPid, pid).ge(MenuEntity::getSort, sort).setIncrBy(MenuEntity::getSort, 1)); + } + + this.save(menuEntity); + Long menuEntityId = menuEntity.getId(); + exists = resourceService.exists(Wrappers.lambdaQuery().eq(ResourceEntity::getSn, sn)); + Assert.isFalse(exists, () -> Exceptions.exception("资源编码重复")); + resourceService.save(new ResourceEntity() + .setSn(sn) + .setDataId(menuEntityId) + .setTableName(ResourceOrigin.Menu.getVal()) + .setMemo("菜单资源-" + menuAddParam.getMenuCategory().getTxt() + "-" + menuAddParam.getTitle()) + ); + return menuEntityId; + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(MenuModifyParam menuModifyParam) { + MenuEntity menuEntity = BeanUtil.copyProperties(menuModifyParam, MenuEntity.class); + Long id = menuEntity.getId(); + Long pid = menuEntity.getPid(); + MenuEntity oldMenuEntity = this.getById(id); + Assert.notNull(oldMenuEntity, () -> Exceptions.exception("菜单不存在")); + MenuCategory oldMenuCategory = oldMenuEntity.getMenuCategory(); + String routeName = menuEntity.getRouteName(); + + if (oldMenuCategory == MenuCategory.Page || oldMenuCategory == MenuCategory.SubPage && routeName != null) { + Assert.notBlank(routeName, () -> Exceptions.exception("路由名称不能为空")); + } else if (oldMenuCategory == MenuCategory.Catalog || oldMenuCategory == MenuCategory.Btn && routeName != null) { + Assert.isTrue(StrUtil.isBlank(routeName), () -> Exceptions.exception("菜单目录和按钮不用指定路由名称")); + } + Long oldPid = oldMenuEntity.getPid(); + if (pid != null && !pid.equals(oldPid)) { + MenuEntity parent = this.getById(pid); + Assert.notNull(parent, () -> Exceptions.exception("上级菜单不存在")); + String title = menuEntity.getTitle(); + List breadcrumb = parent.getBreadcrumb(); + breadcrumb.add(StrUtil.isBlank(title) ? oldMenuEntity.getTitle() : title); + menuEntity.setBreadcrumb(breadcrumb) + .setTier(parent.getTier() + 1); + + this.modifyChild(menuEntity); + } + + Integer sort = menuEntity.getSort(); + if (sort != null && sort != 0) { + List list = this.list(Wrappers.lambdaQuery() + .eq(MenuEntity::getPid, pid == null ? oldPid : pid) + .ge(MenuEntity::getSort, sort) + .ne(MenuEntity::getId, id) + .select(MenuEntity::getSort, MenuEntity::getId) + ); + if (CollUtil.isNotEmpty(list)) { + list.forEach(item -> item.setSort(sort + 1)); + this.updateBatchById(list); + } + } + + this.updateById(menuEntity); + + String title = menuModifyParam.getTitle(); + if (StrUtil.isBlank(title) || oldMenuEntity.getTitle().equals(title)) return; + resourceService.update(Wrappers.lambdaUpdate() + .eq(ResourceEntity::getTableName, ResourceOrigin.Menu.getVal()) + .eq(ResourceEntity::getDataId, id) + .set(ResourceEntity::getMemo, "菜单资源-" + oldMenuEntity.getMenuCategory().getTxt() + "-" + title)); + } + + private void modifyChild(MenuEntity menuEntity) { + MenuCategory menuCategory = menuEntity.getMenuCategory(); + if (menuCategory == MenuCategory.SubPage || menuCategory == MenuCategory.Btn) return; + + List breadcrumb = menuEntity.getBreadcrumb(); + int tier = menuEntity.getTier() + 1; + Long pid = menuEntity.getId(); + + + List children = this.list(Wrappers.lambdaQuery().eq(MenuEntity::getPid, pid)) + .stream() + .map(it -> { + List temp = CollUtil.newArrayList(breadcrumb); + temp.add(it.getTitle()); + return new MenuEntity() + .setId(it.getId()) + .setBreadcrumb(temp) + .setTier(tier); + }).collect(Collectors.toList()); + + // noinspection SpringTransactionalMethodCallsInspection + this.updateBatchById(children); + + children.forEach(this::modifyChild); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + boolean occupied = resourceService.occupied(ids, ResourceOrigin.Menu); + Assert.isFalse(occupied, () -> Exceptions.exception("菜单已被分配,不能删除")); + + this.removeBatchByIds(ids); + resourceService.delRes(ids, ResourceOrigin.Menu); + } + + /** + * 详情 + */ + public MenuDetailResult detail(Long id) { + return BeanUtil.copyProperties(this.getById(id), MenuDetailResult.class); + } + + /** + * 查询所有 + */ + public List listAll(MenuSearchParam menuSearchParam) { + Long pid = menuSearchParam.getPid(); + String title = menuSearchParam.getTitle(); + String routeName = menuSearchParam.getRouteName(); + return this.list(Wrappers.lambdaQuery() + .eq(pid != null, MenuEntity::getPid, pid) + .like(StrUtil.isNotBlank(title), MenuEntity::getTitle, title) + .like(StrUtil.isNotBlank(routeName), MenuEntity::getRouteName, routeName) + .orderByAsc(Arrays.asList(MenuEntity::getTier, MenuEntity::getSort, MenuEntity::getId))) + .stream() + .map(it -> BeanUtil.copyProperties(it, MenuDetailResult.class)) + .collect(Collectors.toList()); + } + + public PageResult paging(PageParam pageParam, MenuSearchParam menuSearchParam) { + Long pid = menuSearchParam.getPid(); + if (pid == null) { + pid = 0L; + } + String title = menuSearchParam.getTitle(); + String routeName = menuSearchParam.getRouteName(); + Page page = this.page(pageParam.toPage(), Wrappers.lambdaQuery() + .eq(MenuEntity::getPid, pid) + .like(StrUtil.isNotBlank(title), MenuEntity::getTitle, title) + .like(StrUtil.isNotBlank(routeName), MenuEntity::getRouteName, routeName) + .orderByAsc(Arrays.asList(MenuEntity::getTier, MenuEntity::getSort, MenuEntity::getId))); + return PageResult.of(page.convert(it -> BeanUtil.copyProperties(it, MenuDetailResult.class))); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/contant/ResourceOrigin.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/contant/ResourceOrigin.java new file mode 100644 index 0000000..21152f7 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/contant/ResourceOrigin.java @@ -0,0 +1,19 @@ +package com.njzscloud.supervisory.sys.resource.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 资源 + */ +@Getter +@RequiredArgsConstructor +public enum ResourceOrigin implements DictStr { + Menu("sys_menu", "菜单"), + Endpoint("sys_endpoint", "接口"), + ; + + private final String val; + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/cotroller/ResourceController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/cotroller/ResourceController.java new file mode 100644 index 0000000..1dc08c9 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/cotroller/ResourceController.java @@ -0,0 +1,44 @@ +package com.njzscloud.supervisory.sys.resource.cotroller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.supervisory.sys.resource.pojo.entity.ResourceEntity; +import com.njzscloud.supervisory.sys.resource.service.ResourceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 系统资源表 + */ +@Slf4j +@RestController +@RequestMapping("/resource") +@RequiredArgsConstructor +public class ResourceController { + private final ResourceService resourceService; + + @GetMapping("/list") + public R> list( + @RequestParam(value = "tableName", required = false) String tableName, + @RequestParam(value = "keywords", required = false) String keywords + ) { + return R.success(resourceService.list(Wrappers.lambdaQuery() + .eq(StrUtil.isNotBlank(tableName), ResourceEntity::getTableName, tableName) + .and(StrUtil.isNotBlank(keywords), ew -> ew.like(ResourceEntity::getSn, keywords) + .or().like(ResourceEntity::getMemo, keywords)) + )); + } + + @GetMapping("/list_role_res") + public R> listRoleRes(@RequestParam(value = "roleId") String roleId) { + return R.success(resourceService.listRoleRes(roleId)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/mapper/ResourceMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/mapper/ResourceMapper.java new file mode 100644 index 0000000..4c88fcb --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/mapper/ResourceMapper.java @@ -0,0 +1,20 @@ +package com.njzscloud.supervisory.sys.resource.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.supervisory.sys.resource.pojo.entity.ResourceEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 系统资源表 + */ +@Mapper +public interface ResourceMapper extends BaseMapper { + List occupied(@Param(Constants.WRAPPER) QueryWrapper ew); + + List listRoleRes(@Param("roleId") String roleId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/pojo/entity/ResourceEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/pojo/entity/ResourceEntity.java new file mode 100644 index 0000000..846dde4 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/pojo/entity/ResourceEntity.java @@ -0,0 +1,45 @@ +package com.njzscloud.supervisory.sys.resource.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 系统资源表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_resource") +public class ResourceEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 编号 + */ + private String sn; + + /** + * 表名称 + */ + private String tableName; + + /** + * 数据行 Id + */ + private Long dataId; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/service/ResourceService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/service/ResourceService.java new file mode 100644 index 0000000..b6829a2 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/resource/service/ResourceService.java @@ -0,0 +1,48 @@ +package com.njzscloud.supervisory.sys.resource.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.supervisory.sys.resource.contant.ResourceOrigin; +import com.njzscloud.supervisory.sys.resource.mapper.ResourceMapper; +import com.njzscloud.supervisory.sys.resource.pojo.entity.ResourceEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 系统资源表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ResourceService extends ServiceImpl implements IService { + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void delRes(List ids, ResourceOrigin resourceOrigin) { + this.remove(Wrappers.lambdaQuery() + .eq(ResourceEntity::getTableName, resourceOrigin.getVal()) + .in(ResourceEntity::getDataId, ids) + ); + } + + /** + * 校验是否被占用 + */ + public boolean occupied(List dataIds, ResourceOrigin resourceOrigin) { + List roles = baseMapper.occupied(Wrappers.query() + .eq("a.table_name", resourceOrigin.getVal()) + .in("a.data_id", dataIds)); + return !roles.isEmpty(); + } + + public List listRoleRes(String roleId) { + return baseMapper.listRoleRes(roleId); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/controller/RoleController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/controller/RoleController.java new file mode 100644 index 0000000..89b33cb --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/controller/RoleController.java @@ -0,0 +1,102 @@ +package com.njzscloud.supervisory.sys.role.controller; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleEntity; +import com.njzscloud.supervisory.sys.role.pojo.param.BindResParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleAddParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleModifyParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleQueryParam; +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import com.njzscloud.supervisory.sys.role.service.RoleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 角色 + */ +@Slf4j +@RestController +@RequestMapping("/role") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody @Validated RoleAddParam roleAddParam) { + return R.success(roleService.add(roleAddParam)); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody RoleModifyParam roleModifyParam) { + roleService.modify(roleModifyParam); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, "未指定要删除的数据"); + roleService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam("id") Long id) { + return R.success(roleService.detail(id)); + } + + /** + * 列表 + */ + @GetMapping("/list") + public R> list(@RequestParam(required = false, value = "roleName") String roleName) { + return R.success(roleService.list(Wrappers.lambdaQuery(RoleEntity.class).like(StrUtil.isNotBlank(roleName), RoleEntity::getRoleName, roleName))); + } + + /** + * 绑定资源 + */ + @PostMapping("/bind_res") + public R bindRole(@RequestBody BindResParam bindResParam) { + roleService.bindRes(bindResParam); + return R.success(); + } + + /** + * 查询用户拥有的角色 + */ + @GetMapping("/list_user_role") + public R> listUserRole(@RequestParam(value = "userId") Long userId) { + return R.success(roleService.listUserRole(userId)); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam page, RoleQueryParam roleQueryParam) { + return R.success(roleService.paging(page.toPage(), roleQueryParam)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleMapper.java new file mode 100644 index 0000000..29a5e8a --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleMapper.java @@ -0,0 +1,29 @@ +package com.njzscloud.supervisory.sys.role.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

sys_role

+ *

角色表

+ */ +@Mapper +public interface RoleMapper extends BaseMapper { + + /** + * 分页查询 + */ + IPage paging(IPage page, @Param(Constants.WRAPPER) Wrapper ew); + + List listUserRole(@Param("userId") Long userId); + + Boolean candel(@Param("ew") QueryWrapper ew); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleResMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleResMapper.java new file mode 100644 index 0000000..f741eea --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/mapper/RoleResMapper.java @@ -0,0 +1,13 @@ +package com.njzscloud.supervisory.sys.role.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleResourceEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 角色-资源关系表 + */ +@Mapper +public interface RoleResMapper extends BaseMapper { + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleEntity.java new file mode 100644 index 0000000..2e4ef54 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleEntity.java @@ -0,0 +1,40 @@ +package com.njzscloud.supervisory.sys.role.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 角色表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_role") +public class RoleEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 角色代码; 以 ROLE_ 开头 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleResourceEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleResourceEntity.java new file mode 100644 index 0000000..af46f60 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/entity/RoleResourceEntity.java @@ -0,0 +1,51 @@ +package com.njzscloud.supervisory.sys.role.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 角色-资源关系表 + */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_role_resource") +public class RoleResourceEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 角色 Id; sys_role.id + */ + private Long roleId; + + /** + * 资源 Id; sys_resource.id + */ + private Long resId; + + /** + * 资源编码; sys_resource.sn + */ + private String resSn; + + + /** + * 表名称 + */ + private String tableName; + + /** + * 数据行 Id + */ + private Long dataId; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/BindResParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/BindResParam.java new file mode 100644 index 0000000..0f7b11c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/BindResParam.java @@ -0,0 +1,20 @@ +package com.njzscloud.supervisory.sys.role.pojo.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Set; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class BindResParam { + private Long id; + /** + * 资源 Id 列表 + */ + private Set res; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleAddParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleAddParam.java new file mode 100644 index 0000000..2e8bcdc --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleAddParam.java @@ -0,0 +1,32 @@ +package com.njzscloud.supervisory.sys.role.pojo.param; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.Set; + +@Getter +@Setter +@ToString +public class RoleAddParam { + /** + * 角色代码 + */ + @NotBlank(message = "角色代码不能为空") + private String roleCode; + + /** + * 角色名称 + */ + @NotBlank(message = "角色名称不能为空") + private String roleName; + + /** + * 备注 + */ + private String memo; + + private Set resIds; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleModifyParam.java new file mode 100644 index 0000000..f70a4a6 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleModifyParam.java @@ -0,0 +1,34 @@ +package com.njzscloud.supervisory.sys.role.pojo.param; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.Set; + +@Getter +@Setter +@ToString +public class RoleModifyParam { + @NotNull(message = "Id 不能为空") + private Long id; + + /** + * 角色代码 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + + private Set resIds; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleQueryParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleQueryParam.java new file mode 100644 index 0000000..ee3423c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/param/RoleQueryParam.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.sys.role.pojo.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class RoleQueryParam { + /** + * 角色代码 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/result/RoleDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/result/RoleDetailResult.java new file mode 100644 index 0000000..e5daf10 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/pojo/result/RoleDetailResult.java @@ -0,0 +1,40 @@ +package com.njzscloud.supervisory.sys.role.pojo.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + *

sys_role

+ *

角色表

+ */ +@Getter +@Setter +@Accessors(chain = true) +public class RoleDetailResult { + + /** + * Id + */ + private Long id; + + /** + * 角色代码; 以 ROLE_ 开头 + */ + private String roleCode; + + /** + * 角色名称 + */ + private String roleName; + + /** + * 备注 + */ + private String memo; + + private List resIds; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleResService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleResService.java new file mode 100644 index 0000000..83a5fe9 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleResService.java @@ -0,0 +1,116 @@ +package com.njzscloud.supervisory.sys.role.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.utils.GroupUtil; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.resource.pojo.entity.ResourceEntity; +import com.njzscloud.supervisory.sys.resource.service.ResourceService; +import com.njzscloud.supervisory.sys.role.mapper.RoleResMapper; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleResourceEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 角色-资源关系表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RoleResService extends ServiceImpl implements IService { + private final ResourceService resourceService; + + /** + * 新增 + * + * @param roleResourceEntity 数据 + */ + + public void add(RoleResourceEntity roleResourceEntity) { + this.save(roleResourceEntity); + } + + /** + * 修改 + * + * @param roleResourceEntity 数据 + */ + + public void modify(RoleResourceEntity roleResourceEntity) { + this.updateById(roleResourceEntity); + } + + /** + * 删除 + * + * @param ids Ids + */ + + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + this.removeBatchByIds(ids); + } + + /** + * 详情 + * + * @param id Id + * @return SysRoleResourceEntity 结果 + */ + + public RoleResourceEntity detail(Long id) { + return this.getById(id); + } + + /** + * 分页查询 + * + * @param roleResourceEntity 筛选条件 + * @param pageParam 分页参数 + * @return PageResult<SysRoleResourceEntity> 分页结果 + */ + + public PageResult paging(PageParam pageParam, RoleResourceEntity roleResourceEntity) { + return PageResult.of(this.page(pageParam.toPage(), Wrappers.query(roleResourceEntity))); + } + + /** + * 为角色绑定资源 + */ + @Transactional(rollbackFor = Exception.class) + public void bindRoleRes(Long roleId, Set resIds) { + if (CollUtil.isEmpty(resIds)) return; + List sysMenuEntities = resourceService.listByIds(resIds); + Assert.isTrue(sysMenuEntities.size() == resIds.size(), () -> Exceptions.exception("资源不存在")); + + this.remove(Wrappers.lambdaQuery().eq(RoleResourceEntity::getRoleId, roleId)); + + Map map = GroupUtil.k_o(sysMenuEntities, ResourceEntity::getId); + + List list = resIds.stream().map(it -> { + ResourceEntity resourceEntity = map.get(it); + return new RoleResourceEntity() + .setRoleId(roleId) + .setResId(it) + .setResSn(resourceEntity.getSn()) + .setTableName(resourceEntity.getTableName()) + .setDataId(resourceEntity.getDataId()); + }).collect(Collectors.toList()); + + this.saveBatch(list); + } + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleService.java new file mode 100644 index 0000000..303b261 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/role/service/RoleService.java @@ -0,0 +1,130 @@ +package com.njzscloud.supervisory.sys.role.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.role.mapper.RoleMapper; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleEntity; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleResourceEntity; +import com.njzscloud.supervisory.sys.role.pojo.param.BindResParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleAddParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleModifyParam; +import com.njzscloud.supervisory.sys.role.pojo.param.RoleQueryParam; +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.njzscloud.common.security.contant.Constants.ROLE_ADMIN; + +/** + * 角色表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RoleService extends ServiceImpl implements IService { + private final RoleResService roleResService; + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public Long add(RoleAddParam roleAddParam) { + RoleEntity roleEntity = BeanUtil.copyProperties(roleAddParam, RoleEntity.class); + String roleCode = roleEntity.getRoleCode(); + if (!roleCode.startsWith("ROLE_")) { + roleCode = "ROLE_" + roleCode; + roleEntity.setRoleCode(roleCode); + } + boolean exists = this.exists(Wrappers.lambdaQuery().eq(RoleEntity::getRoleCode, roleCode)); + String roleCode_ = roleCode; + Assert.isFalse(exists, () -> Exceptions.clierr("角色编码:{} 已存在", roleCode_)); + this.save(roleEntity); + + Long roleEntityId = roleEntity.getId(); + Set resIds = roleAddParam.getResIds(); + roleResService.bindRoleRes(roleEntityId, resIds); + return roleEntityId; + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(RoleModifyParam roleModifyParam) { + Long roleId = roleModifyParam.getId(); + boolean exists = this.exists(Wrappers.lambdaQuery().eq(RoleEntity::getId, roleId)); + Assert.isTrue(exists, () -> Exceptions.clierr("角色不存在")); + RoleEntity roleEntity = BeanUtil.copyProperties(roleModifyParam, RoleEntity.class); + this.updateById(roleEntity); + Set resIds = roleModifyParam.getResIds(); + roleResService.bindRoleRes(roleId, resIds); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + Assert.notEmpty(ids, () -> Exceptions.clierr("未指定要删除的数据")); + List list = this.listByIds(ids); + Assert.isFalse(list.stream().anyMatch(it -> { + String roleCode = it.getRoleCode(); + return roleCode.equals(ROLE_ADMIN); + } + ), () -> Exceptions.exception("管理员角色不允许删除")); + Boolean candel = baseMapper.candel(Wrappers.query().in("a.id", ids)); + Assert.isTrue(candel, () -> Exceptions.exception("角色已在使用不能删除")); + this.removeBatchByIds(ids); + roleResService.remove(Wrappers.lambdaQuery().in(RoleResourceEntity::getRoleId, ids)); + } + + /** + * 详情 + */ + public RoleDetailResult detail(Long id) { + RoleEntity roleEntity = this.getById(id); + List list = roleResService.list(Wrappers.lambdaQuery().eq(RoleResourceEntity::getRoleId, id)); + List resIds = list.stream().map(RoleResourceEntity::getResId).collect(Collectors.toList()); + return BeanUtil.copyProperties(roleEntity, RoleDetailResult.class) + .setResIds(resIds); + } + + /** + * 分页查询 + */ + public PageResult paging(IPage page, RoleQueryParam roleQueryParam) { + String roleCode = roleQueryParam.getRoleCode(); + String roleName = roleQueryParam.getRoleName(); + Wrapper ew = Wrappers.lambdaQuery() + .like(StrUtil.isNotBlank(roleCode), RoleEntity::getRoleCode, roleCode) + .like(StrUtil.isNotBlank(roleName), RoleEntity::getRoleName, roleName); + return PageResult.of(baseMapper.paging(page, ew).convert(it -> BeanUtil.copyProperties(it, RoleDetailResult.class))); + } + + public List listUserRole(Long userId) { + return baseMapper.listUserRole(userId); + } + + public void bindRes(BindResParam bindResParam) { + Long id = bindResParam.getId(); + RoleEntity userEntity = this.getById(id); + Assert.notNull(userEntity, () -> Exceptions.clierr("角色不存在")); + Set res = bindResParam.getRes(); + Assert.notEmpty(res, () -> Exceptions.clierr("未指定资源")); + roleResService.bindRoleRes(id, res); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/contant/Gender.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/contant/Gender.java new file mode 100644 index 0000000..b5f1215 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/contant/Gender.java @@ -0,0 +1,21 @@ +package com.njzscloud.supervisory.sys.user.contant; + +import com.njzscloud.common.core.ienum.DictStr; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 字典代码:gender + * 字典名称:性别 + */ +@Getter +@RequiredArgsConstructor +public enum Gender implements DictStr { + Unknown("Unknown", "未知"), + Man("Man", "男"), + Woman("Woman", "女"); + + private final String val; + + private final String txt; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/controller/UserController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/controller/UserController.java new file mode 100644 index 0000000..9b9924c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/controller/UserController.java @@ -0,0 +1,126 @@ +package com.njzscloud.supervisory.sys.user.controller; + +import cn.hutool.core.lang.Assert; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.core.utils.R; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.supervisory.sys.user.pojo.param.*; +import com.njzscloud.supervisory.sys.user.pojo.result.UserDetailResult; +import com.njzscloud.supervisory.sys.user.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 用户信息 + */ +@Slf4j +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + /** + * 新增 + */ + @PostMapping("/add") + public R add(@RequestBody @Validated AddUserParam addUserParam) { + return R.success(userService.add(addUserParam)); + } + + /** + * 修改 + */ + @PostMapping("/modify") + public R modify(@RequestBody @Validated UserModifyParam userModifyParam) { + userService.modify(userModifyParam); + return R.success(); + } + + /** + * 删除 + */ + @PostMapping("/del") + public R del(@RequestBody List ids) { + Assert.notEmpty(ids, () -> Exceptions.clierr("未指定要删除的数据")); + userService.del(ids); + return R.success(); + } + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(@RequestParam("id") Long id) { + return R.success(userService.detail(id)); + } + + /** + * 禁用 + */ + @GetMapping("/disable") + public R detail(@RequestParam("id") Long id, @RequestParam("disable") Boolean disable) { + userService.disable(id, disable); + return R.success(); + } + + /** + * 绑定角色 + */ + @PostMapping("/bind_role") + public R bindRole(@RequestBody BindRoleParam bindRoleParam) { + userService.bindRole(bindRoleParam); + return R.success(); + } + + /** + * 授权客户端 + */ + @GetMapping("/bind_client") + public R bindClient(@RequestParam("id") Long id, @RequestParam("clientCode") Integer clientCode) { + userService.bindClient(id, clientCode); + return R.success(); + } + + /** + * 分页查询 + */ + @GetMapping("/paging") + public R> paging(PageParam pageParam, UserQueryParam userQueryParam) { + return R.success(userService.paging(pageParam, userQueryParam)); + } + + /** + * 用户注册 + * + * @param userRegisterParam 参数 + */ + @PostMapping("/register") + public R register(@RequestBody @Validated UserRegisterParam userRegisterParam) { + return R.success(userService.register(userRegisterParam)); + } + + /** + * 修改密码 + */ + @PostMapping("/modify_passwd") + public R modifyPasswd(@RequestBody @Validated ModifyPasswdParam modifyPasswdParam) { + userService.modifyPasswd(modifyPasswdParam); + return R.success(); + } + + /** + * 重置密码 + */ + @GetMapping("/reset_passwd") + public R resetPasswd(@RequestParam("id") Long id) { + userService.resetPasswd(id); + return R.success(true, "重置成功,新密码:8个8"); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/SysUserRoleMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..c1d70e7 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/SysUserRoleMapper.java @@ -0,0 +1,18 @@ +package com.njzscloud.supervisory.sys.user.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import com.njzscloud.supervisory.sys.user.pojo.entity.SysUserRoleEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户-角色关系表 + */ +@Mapper +public interface SysUserRoleMapper extends BaseMapper { + List listRole(@Param("ew") QueryWrapper ew); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserAccountMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserAccountMapper.java new file mode 100644 index 0000000..f8a56d0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserAccountMapper.java @@ -0,0 +1,12 @@ +package com.njzscloud.supervisory.sys.user.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njzscloud.supervisory.sys.user.pojo.entity.UserAccountEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户账号信息表 + */ +@Mapper +public interface UserAccountMapper extends BaseMapper { +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserMapper.java new file mode 100644 index 0000000..5887a6c --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/mapper/UserMapper.java @@ -0,0 +1,29 @@ +package com.njzscloud.supervisory.sys.user.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.supervisory.sys.user.pojo.entity.UserEntity; +import com.njzscloud.supervisory.sys.user.pojo.result.UserDetailResult; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 用户信息 + */ +@Mapper +public interface UserMapper extends BaseMapper { + + /** + * 分页查询 + * + * @param page 分页参数 + * @param ew 查询条件 + * @return IPage<UserDetailResult> 分页结果 + */ + IPage paging(IPage page, @Param(Constants.WRAPPER) Wrapper ew); + + UserDetail selectUser(@Param("userId") Long userId); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/SysUserRoleEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/SysUserRoleEntity.java new file mode 100644 index 0000000..6e68238 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/SysUserRoleEntity.java @@ -0,0 +1,35 @@ +package com.njzscloud.supervisory.sys.user.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + *

用户-角色关系表

+ */ +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_user_role") +public class SysUserRoleEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + + /** + * 角色 Id; sys_role.id + */ + private Long roleId; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserAccountEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserAccountEntity.java new file mode 100644 index 0000000..f775811 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserAccountEntity.java @@ -0,0 +1,103 @@ +package com.njzscloud.supervisory.sys.user.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + *

sys_user_account

+ *

用户账号信息表

+ */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_user_account") +public class UserAccountEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 手机号 + */ + private String phone; + + /** + * 密码 + */ + private String secret; + + /** + * 微信 openid + */ + private String wechatOpenid; + + /** + * 微信 unionid + */ + private String wechatUnionid; + + /** + * 注册时间 + */ + private LocalDateTime regdate; + + /** + * 允许登录的客户端;位权, 0-->PC、1-->移动端、2-->小程序 + */ + private Integer clientCode; + + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserEntity.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserEntity.java new file mode 100644 index 0000000..e2e2f80 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/entity/UserEntity.java @@ -0,0 +1,71 @@ +package com.njzscloud.supervisory.sys.user.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 用户信息表 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@TableName("sys_user") +public class UserEntity { + + /** + * Id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 手机号 + */ + private String phone; + + /** + * 创建人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT) + private Long creatorId; + + /** + * 修改人 Id; sys_user.id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long modifierId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime modifyTime; + + /** + * 是否删除; 0-->未删除、1-->已删除 + */ + @TableLogic + private Boolean deleted; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserAccountParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserAccountParam.java new file mode 100644 index 0000000..34eda68 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserAccountParam.java @@ -0,0 +1,64 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Setter +@Constraint +@Accessors(chain = true) +public class AddUserAccountParam implements Constrained { + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + private String username; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 密码 + */ + private String secret; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> (StrUtil.isNotBlank(username) && StrUtil.isNotBlank(secret)) + || (StrUtil.isNotBlank(email) && StrUtil.isNotBlank(secret)) + || (StrUtil.isNotBlank(phone) && StrUtil.isNotBlank(secret)) + || StrUtil.isNotBlank(wechatOpenid), "账号信息不能为空"), + }; + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserParam.java new file mode 100644 index 0000000..2bb5237 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/AddUserParam.java @@ -0,0 +1,58 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.supervisory.sys.user.contant.Gender; +import jakarta.validation.Valid; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Set; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class AddUserParam implements Constrained { + /** + * 昵称 + */ + private String nickname; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + /** + * 账号信息 + */ + @Valid + private AddUserAccountParam account; + /** + * 角色 Id 列表 + */ + private Set roles; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(nickname), "用户昵称不能为空"), + ValidRule.of(() -> account != null, "账号信息不能为空"), + }; + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/BindRoleParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/BindRoleParam.java new file mode 100644 index 0000000..4ba06cf --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/BindRoleParam.java @@ -0,0 +1,20 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Set; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class BindRoleParam { + private Long id; + /** + * 角色 Id 列表 + */ + private Set roles; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/ModifyPasswdParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/ModifyPasswdParam.java new file mode 100644 index 0000000..1007ada --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/ModifyPasswdParam.java @@ -0,0 +1,16 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Setter +@Accessors(chain = true) +public class ModifyPasswdParam { + @NotBlank(message = "旧密码不能为空") + private String oldPasswd; + @NotBlank(message = "新密码不能为空") + private String newPasswd; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserModifyParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserModifyParam.java new file mode 100644 index 0000000..e9e30e8 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserModifyParam.java @@ -0,0 +1,38 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import com.njzscloud.supervisory.sys.user.contant.Gender; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + + +@Getter +@Setter +public class UserModifyParam { + @NotNull + private Long id; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 角色 Id 列表 + */ + private Set roles; + + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserQueryParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserQueryParam.java new file mode 100644 index 0000000..0be5fe1 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserQueryParam.java @@ -0,0 +1,37 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import com.njzscloud.supervisory.sys.user.contant.Gender; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserQueryParam { + /** + * 昵称 + */ + private String nickname; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + /** + * 用户名 + */ + private String username; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserRegisterParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserRegisterParam.java new file mode 100644 index 0000000..80e4e12 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/param/UserRegisterParam.java @@ -0,0 +1,118 @@ +package com.njzscloud.supervisory.sys.user.pojo.param; + +import cn.hutool.core.util.StrUtil; +import com.njzscloud.common.mvc.validator.Constrained; +import com.njzscloud.common.mvc.validator.Constraint; +import com.njzscloud.common.mvc.validator.ValidRule; +import com.njzscloud.common.security.contant.AuthWay; +import com.njzscloud.supervisory.sys.user.contant.Gender; +import jakarta.validation.Valid; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class UserRegisterParam implements Constrained { + private AuthWay authWay; + /** + * 昵称 + */ + private String nickname; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 头像 + */ + private String avatar; + + /** + * 性别; 字典代码:gender + */ + private Gender gender; + /** + * 账号信息 + */ + @Valid + private Account account; + /** + * 角色 Id 列表 + */ + private List roles; + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> StrUtil.isNotBlank(nickname), "用户昵称不能为空"), + ValidRule.of(() -> account != null, "账号信息不能为空"), + }; + } + + + @Getter + @Setter + @Constraint + @Accessors(chain = true) + public static class Account implements Constrained { + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + private String username; + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 密码 + */ + private String secret; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; + + + @Override + public ValidRule[] rules() { + return new ValidRule[]{ + ValidRule.of(() -> (StrUtil.isNotBlank(username) && StrUtil.isNotBlank(secret)) + || (StrUtil.isNotBlank(email) && StrUtil.isNotBlank(secret)) + || (StrUtil.isNotBlank(phone) && StrUtil.isNotBlank(secret)) + || StrUtil.isNotBlank(wechatOpenid), "账号信息不能为空"), + }; + } + + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserAccountDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserAccountDetailResult.java new file mode 100644 index 0000000..c7aacb4 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserAccountDetailResult.java @@ -0,0 +1,43 @@ +package com.njzscloud.supervisory.sys.user.pojo.result; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class UserAccountDetailResult { + private Long id; + + /** + * 用户 Id; sys_user.id + */ + private Long userId; + /** + * 用户名 + */ + private String username; + /** + * 手机号 + */ + private String phone; + + private LocalDateTime regdate; + /** + * 微信 openid + */ + private String wechatOpenid; + /** + * 微信 unionid + */ + private String wechatUnionid; + /** + * 允许登录的客户端; 字典代码:client_code + */ + private Integer clientCode; + /** + * 是否禁用; 0-->启用、1-->禁用 + */ + private Boolean disabled; +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserDetailResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserDetailResult.java new file mode 100644 index 0000000..51da5d5 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/pojo/result/UserDetailResult.java @@ -0,0 +1,40 @@ +package com.njzscloud.supervisory.sys.user.pojo.result; + +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; + +@Getter +@Setter +@Accessors(chain = true) +public class UserDetailResult { + private Long id; + + /** + * 昵称 + */ + private String nickname; + + private String avatar; + + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + /** + * 账号信息 + */ + private UserAccountDetailResult account; + /** + * 角色列表 + */ + private List roles; + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserAccountService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserAccountService.java new file mode 100644 index 0000000..9cafa2d --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserAccountService.java @@ -0,0 +1,96 @@ +package com.njzscloud.supervisory.sys.user.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.security.util.EncryptUtil; +import com.njzscloud.supervisory.sys.user.mapper.UserAccountMapper; +import com.njzscloud.supervisory.sys.user.pojo.entity.UserAccountEntity; +import com.njzscloud.supervisory.sys.user.pojo.param.AddUserAccountParam; +import com.njzscloud.supervisory.sys.user.pojo.param.ModifyPasswdParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户账号信息表 + */ +@Slf4j +@Service +public class UserAccountService extends ServiceImpl implements IService { + + /** + * 新增 + */ + public void add(AddUserAccountParam addUserAccountParam) { + String username = addUserAccountParam.getUsername(); + String secret = addUserAccountParam.getSecret(); + String email = addUserAccountParam.getEmail(); + String phone = addUserAccountParam.getPhone(); + String wechatOpenid = addUserAccountParam.getWechatOpenid(); + String wechatUnionid = addUserAccountParam.getWechatUnionid(); + + List oldSysUserList = this.list(Wrappers.lambdaQuery() + .eq(StrUtil.isNotBlank(username), UserAccountEntity::getUsername, username) + .or().eq(StrUtil.isNotBlank(phone), UserAccountEntity::getPhone, phone) + .or(StrUtil.isNotBlank(wechatOpenid), it -> it + .eq(UserAccountEntity::getWechatOpenid, wechatOpenid) + .eq(StrUtil.isNotBlank(wechatUnionid), UserAccountEntity::getWechatUnionid, wechatUnionid) + ) + ); + + if (StrUtil.isNotBlank(username)) { + Assert.notBlank(secret, () -> Exceptions.clierr("密码不能为空")); + Assert.isTrue(oldSysUserList.stream().noneMatch(it -> username.equals(it.getUsername())), () -> Exceptions.exception("用户名【{}】已被使用", username)); + } + if (StrUtil.isNotBlank(phone)) { + Assert.notBlank(secret, () -> Exceptions.clierr("密码不能为空")); + Assert.isTrue(oldSysUserList.stream().noneMatch(it -> phone.equals(it.getPhone())), () -> Exceptions.exception("手机号【{}】已被使用", phone)); + } + if (StrUtil.isNotBlank(wechatOpenid)) { + Assert.isTrue(oldSysUserList.stream().noneMatch(it -> { + String unionid = it.getWechatUnionid(); + return wechatOpenid.equals(it.getWechatOpenid()) && (StrUtil.isBlank(wechatUnionid) || wechatUnionid.equals(unionid)); + }), () -> Exceptions.exception("该微信账号已被使用")); + } + + UserAccountEntity userAccountEntity = BeanUtil.copyProperties(addUserAccountParam, UserAccountEntity.class) + .setSecret(EncryptUtil.encrypt(secret)) + .setRegdate(LocalDateTime.now()); + this.save(userAccountEntity); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void delByUserIds(List ids) { + this.remove(Wrappers.lambdaQuery().in(UserAccountEntity::getUserId, ids)); + } + + /** + * 修改密码 + */ + @Transactional(rollbackFor = Exception.class) + public void modifyPasswd(Long userId, ModifyPasswdParam modifyPasswdParam) { + UserAccountEntity accountEntity = this.getOne(Wrappers.lambdaQuery().eq(UserAccountEntity::getUserId, userId)); + Assert.notNull(accountEntity, () -> Exceptions.clierr("未查询到用户信息")); + boolean matches = EncryptUtil.matches(modifyPasswdParam.getOldPasswd(), accountEntity.getSecret()); + Assert.isTrue(matches, () -> Exceptions.exception("密码错误")); + String newPasswd = EncryptUtil.encrypt(modifyPasswdParam.getNewPasswd()); + + Long id = accountEntity.getId(); + + this.updateById(new UserAccountEntity() + .setId(id) + .setSecret(newPasswd) + ); + } +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserRoleService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserRoleService.java new file mode 100644 index 0000000..6bada2b --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserRoleService.java @@ -0,0 +1,62 @@ +package com.njzscloud.supervisory.sys.user.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.supervisory.sys.role.pojo.entity.RoleEntity; +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import com.njzscloud.supervisory.sys.role.service.RoleService; +import com.njzscloud.supervisory.sys.user.mapper.SysUserRoleMapper; +import com.njzscloud.supervisory.sys.user.pojo.entity.SysUserRoleEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + *

用户-角色关系表

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserRoleService extends ServiceImpl implements IService { + private final RoleService roleService; + + /** + * 删除用户角色 + */ + @Transactional(rollbackFor = Exception.class) + public void delByUserIds(List userIdList) { + this.remove(Wrappers.lambdaQuery().in(SysUserRoleEntity::getUserId, userIdList)); + } + + /** + * 获取用户拥有的角色 + */ + public List listRole(Long userId) { + return baseMapper.listRole(Wrappers.query().eq("a.user_id", userId)); + } + + /** + * 绑定用户角色 + */ + @Transactional(rollbackFor = Exception.class) + public void bindUserRole(Long userId, Set roleIds) { + if (CollUtil.isEmpty(roleIds)) return; + + List sysRoleEntities = roleService.listByIds(roleIds); + Assert.isTrue(sysRoleEntities.size() == roleIds.size(), () -> Exceptions.exception("角色不存在")); + + this.remove(Wrappers.lambdaQuery().eq(SysUserRoleEntity::getUserId, userId)); + + this.saveBatch(roleIds.stream().map(roleId -> new SysUserRoleEntity().setRoleId(roleId).setUserId(userId)).collect(Collectors.toList())); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserService.java new file mode 100644 index 0000000..6b30dae --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/sys/user/service/UserService.java @@ -0,0 +1,176 @@ +package com.njzscloud.supervisory.sys.user.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njzscloud.common.core.ex.Exceptions; +import com.njzscloud.common.mp.support.PageParam; +import com.njzscloud.common.mp.support.PageResult; +import com.njzscloud.common.security.support.UserDetail; +import com.njzscloud.common.security.util.EncryptUtil; +import com.njzscloud.common.security.util.SecurityUtil; +import com.njzscloud.supervisory.sys.role.pojo.result.RoleDetailResult; +import com.njzscloud.supervisory.sys.user.contant.Gender; +import com.njzscloud.supervisory.sys.user.mapper.UserMapper; +import com.njzscloud.supervisory.sys.user.pojo.entity.UserAccountEntity; +import com.njzscloud.supervisory.sys.user.pojo.entity.UserEntity; +import com.njzscloud.supervisory.sys.user.pojo.param.*; +import com.njzscloud.supervisory.sys.user.pojo.result.UserAccountDetailResult; +import com.njzscloud.supervisory.sys.user.pojo.result.UserDetailResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 用户信息表 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService extends ServiceImpl implements IService { + private final UserAccountService userAccountService; + private final UserRoleService userRoleService; + + /** + * 新增 + */ + @Transactional(rollbackFor = Exception.class) + public Long add(AddUserParam addUserParam) { + UserEntity userEntity = BeanUtil.copyProperties(addUserParam, UserEntity.class); + this.save(userEntity); + + Long userEntityId = userEntity.getId(); + AddUserAccountParam addUserAccountParam = addUserParam.getAccount(); + + userAccountService.add(addUserAccountParam.setUserId(userEntityId)); + + userRoleService.bindUserRole(userEntityId, addUserParam.getRoles()); + + return userEntityId; + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void modify(UserModifyParam userModifyParam) { + Long id = userModifyParam.getId(); + UserEntity userEntity = this.getById(id); + Assert.notNull(userEntity, () -> Exceptions.clierr("要修改的数据不存在")); + + userEntity = BeanUtil.copyProperties(userModifyParam, UserEntity.class); + this.updateById(userEntity); + + userRoleService.bindUserRole(id, userModifyParam.getRoles()); + } + + /** + * 删除 + */ + @Transactional(rollbackFor = Exception.class) + public void del(List ids) { + if (ids.contains(1L)) { + throw Exceptions.clierr("不能删除管理员"); + } + this.removeByIds(ids); + userAccountService.delByUserIds(ids); + userRoleService.delByUserIds(ids); + } + + /** + * 详情 + */ + public UserDetailResult detail(Long id) { + UserEntity userEntity = this.getById(id); + Assert.notNull(userEntity, "未查询到用户信息"); + UserDetailResult userDetailResult = BeanUtil.copyProperties(userEntity, UserDetailResult.class); + UserAccountEntity userAccountEntity = userAccountService.getOne(Wrappers.lambdaQuery().eq(UserAccountEntity::getUserId, id)); + Assert.notNull(userAccountEntity, "未查询到用户信息"); + UserAccountDetailResult userAccountDetailResult = BeanUtil.copyProperties(userAccountEntity, UserAccountDetailResult.class); + List roleDetailResults = userRoleService.listRole(id); + return userDetailResult + .setAccount(userAccountDetailResult) + .setRoles(roleDetailResults); + } + + /** + * 分页查询 + */ + public PageResult paging(PageParam pageParam, UserQueryParam userQueryParam) { + String nickname = userQueryParam.getNickname(); + Gender gender = userQueryParam.getGender(); + String email = userQueryParam.getEmail(); + String phone = userQueryParam.getPhone(); + String username = userQueryParam.getUsername(); + Boolean disabled = userQueryParam.getDisabled(); + QueryWrapper ew = Wrappers.query() + .eq("a.deleted", 0) + .like(StrUtil.isNotBlank(nickname), "a.nickname", nickname) + .eq(gender != null, "a.gender", gender) + .like(StrUtil.isNotBlank(email), "a.email", email) + .like(StrUtil.isNotBlank(phone), "a.phone", phone) + .like(StrUtil.isNotBlank(username), "b.username", username) + .eq(disabled != null, "b.disabled", disabled); + return PageResult.of(baseMapper.paging(pageParam.toPage(), ew)); + } + + /** + * 用户注册 + * + * @param userRegisterParam 参数 + */ + @Transactional(rollbackFor = Exception.class) + public UserDetail register(UserRegisterParam userRegisterParam) { + AddUserParam addUserParam = BeanUtil.copyProperties(userRegisterParam, AddUserParam.class); + addUserParam.setAccount(BeanUtil.copyProperties(userRegisterParam.getAccount(), AddUserAccountParam.class)); + Long userId = this.add(addUserParam); + List roleDetailResults = userRoleService.listRole(userId); + UserDetail userDetail = baseMapper.selectUser(userId) + .setAuthWay(userRegisterParam.getAuthWay()) + .setRoles(roleDetailResults.stream().map(RoleDetailResult::getRoleCode).collect(Collectors.toSet())); + return SecurityUtil.registrationUser(userDetail); + } + + /** + * 修改密码 + */ + @Transactional(rollbackFor = Exception.class) + public void modifyPasswd(ModifyPasswdParam modifyPasswdParam) { + Long userId = SecurityUtil.currentUserId(); + userAccountService.modifyPasswd(userId, modifyPasswdParam); + } + + public void disable(Long id, Boolean disable) { + userAccountService.update(Wrappers.lambdaUpdate().eq(UserAccountEntity::getUserId, id) + .set(UserAccountEntity::getDisabled, disable)); + } + + public void bindRole(BindRoleParam bindRoleParam) { + Long id = bindRoleParam.getId(); + UserEntity userEntity = this.getById(id); + Assert.notNull(userEntity, () -> Exceptions.clierr("用户不存在")); + Set roles = bindRoleParam.getRoles(); + Assert.notEmpty(roles, () -> Exceptions.clierr("未指定角色")); + userRoleService.bindUserRole(id, roles); + + } + + public void bindClient(Long id, Integer clientCode) { + userAccountService.update(Wrappers.lambdaUpdate().eq(UserAccountEntity::getUserId, id) + .set(UserAccountEntity::getClientCode, clientCode)); + } + + public void resetPasswd(Long id) { + userAccountService.update(Wrappers.lambdaUpdate().eq(UserAccountEntity::getUserId, id) + .set(UserAccountEntity::getSecret, EncryptUtil.encrypt("88888888"))); + } +} diff --git a/njzscloud-svr/src/main/resources/application-demo.yml b/njzscloud-svr/src/main/resources/application-demo.yml new file mode 100644 index 0000000..df6e2fa --- /dev/null +++ b/njzscloud-svr/src/main/resources/application-demo.yml @@ -0,0 +1,50 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/njzscloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: root + security: + auth-ignores: + - /demo/** + redis: + enable: false + pubsub: false + host: localhost + #password: redis + database: 0 + port: 6379 + + mail: + # 邮件服务器 + host: smtp.qq.com + # 发送邮件的账户 + username: lzq@qq.com + # 授权码 + password: lzq + +oss: + type: minio + minio: + endpoint: http://localhost:9090 + access-key: minioadmin + secret-key: sdawi5nEH44wycoxOONSg + bucket-name: zsy + ali: + region: cn-shanghai + endpoint: oss-cn-shanghai.aliyuncs.com + access-key: LTAI5tJJu2WayYchExrT5W1E + secret-key: zllX0ZJ1EwsZXT6dE6swCLgTF4ImGg + bucket-name: cdn-zsy + +mybatis-plus: + tunnel: + enable: false + ssh: + host: 139.224.54.144 + port: 22 + user: root + credentials: D:/我的/再昇云/服务器秘钥/139.224.54.144_YZS_S1.pem + localPort: 33061 + db: + host: localhost + port: 33061 diff --git a/njzscloud-svr/src/main/resources/application-dev.yml b/njzscloud-svr/src/main/resources/application-dev.yml new file mode 100644 index 0000000..cacde46 --- /dev/null +++ b/njzscloud-svr/src/main/resources/application-dev.yml @@ -0,0 +1,47 @@ +spring: + datasource: + url: jdbc:mysql://localhost:33061/njzscloud_dispose?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: admin888999 + security: + auth-ignores: + - /oss/** + +oss: + type: ali + ali: + region: cn-shanghai + endpoint: oss-cn-shanghai.aliyuncs.com + access-key: LTAI5tJJu2WayYchExrT5W1E + secret-key: zllX0ZJ1EwsZXT6dE6swCLgTF4ImGg + bucket-name: cdn-zsy + +sichen: + task: + enable: true + +ssh-tunnel: + enable: true + tunnels: + - enable: true + host: 139.224.54.144 + port: 22 + user: root + # 添加虚拟机参数:-DTUNNEL1_CREDENTIAL=证书文件地址 + credentials: ${TUNNEL1_CREDENTIAL:} + proxy: + - localPort: 33061 + targetHost: localhost + targetPort: 33061 + +wechat: + app-id: wx989ea47a5ddf9bfb + app-secret: 66c98dc487a372acb4f1931b38fee8ff + base-url: https://api.weixin.qq.com + +mqtt: + enabled: true + broker: tcp://139.224.54.144:1883 + client-id: njzscloud-svr1 + username: gps + password: TKG4TV3dF7CeazDnUdCF diff --git a/njzscloud-svr/src/main/resources/application-prod.yml b/njzscloud-svr/src/main/resources/application-prod.yml new file mode 100644 index 0000000..9891674 --- /dev/null +++ b/njzscloud-svr/src/main/resources/application-prod.yml @@ -0,0 +1,87 @@ +spring: + application: + name: greenfrog + datasource: + url: jdbc:mysql://rm-uf6g259329on1376u.mysql.rds.aliyuncs.com:3306/greenfrog?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: zsy + password: DtRcBCWYmMSd7Vre2jT + security: + auth-ignores: + - /auth/obtain_code + - /user/register + - /oss/** + - /district/tree + - /sys_sn_config/add + - /test/** + - /device_info/report1 + - /device_info/report2 + - /geo_fence_config/** + - /statistics/** + - /district/default_place + - /bulletin/detail + - /bulletin/paging + - /truck_location_track/** + - /fdx + - /payment/wechat/notify + - /payment/wechat/refundNotify + - /district/areaList + - /biz_audit_config/copy + - /wechatTemplateMessage/key + - /hsoa/push_order + +app: + in-out-gap: 180 + default-place: + province: 340000 + city: 341100 + province-name: 安徽省 + city-name: 滁州市 + +oss: + type: ali + ali: + region: cn-shanghai + endpoint: oss-cn-shanghai.aliyuncs.com + access-key: LTAI5t96qDgWDzu5rSRHUttZ + secret-key: GS9wh0stna11POzBXG7kzh0lKYvhkg + bucket-name: czxcsy-cdn + +mybatis-plus: + tunnel: + enable: false + +wechat: + # app-id: wx3c06d9dd4e56c58d + # app-secret: ff280a71a4c06fc2956178f8c472ef96 + app-id: wx989ea47a5ddf9bfb + app-secret: 66c98dc487a372acb4f1931b38fee8ff + base-url: https://api.weixin.qq.com + pay: + app-id: wx989ea47a5ddf9bfb + mch-id: 1729703110 + # API密钥(32位字符串) + api-key: KXM36nZCXji1sQt75tGk77k7b2K5RBpf + # 证书序列号 + cert-serial-no: 1BCB1533688F349541C7B636EF67C666828BADBA + # 文件路径 + private-key-path: classpath:cert/apiclient_cert.p12 + # private-key-path: D:/project/再昇云/代码/njzscloud/njzscloud-svr/src/main/resources/cert/apiclient_cert.p12 + # 支付回调地址 + notify-url: http://139.224.32.69:80/api/payment/wechat/notify + # 退款回调地址 + refund-notify-url: http://139.224.32.69:80/api/payment/wechat/refundNotify + +mqtt: + enabled: true + broker: tcp://127.0.0.1:1883 + client-id: greenfrog + username: zsy + password: zsy_mqtt_cli + +localizer: + enabled: false + +hsoa: + base-url: http://117.68.7.91:8088 + username: chuz_trajectory + password: e9t2YsgM5ug%2FkpIZpMdY9e9uXq60jyEQ30zQX%2BBzphI%3D diff --git a/njzscloud-svr/src/main/resources/application.yml b/njzscloud-svr/src/main/resources/application.yml new file mode 100644 index 0000000..5efb6d8 --- /dev/null +++ b/njzscloud-svr/src/main/resources/application.yml @@ -0,0 +1,73 @@ +server: + port: ${APP_PORT:10086} + tomcat: + max-http-form-post-size: 20MB +spring: + servlet: + multipart: + max-file-size: 20MB + max-request-size: 20MB + file-size-threshold: 1MB + resolve-lazily: true + profiles: + active: ${APP_PROFILE:dev} + web: + resources: + add-mappings: false + mvc: + format: + date: yyyy-MM-dd + time: HH:mm:ss + date-time: yyyy-MM-dd HH:mm:ss + jackson: + locale: zh_CN + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + default-property-inclusion: always + serialization: + # 对象不含任何字段时是否报错 + fail-on-empty-beans: false + # 是否捕获并且包装异常信息 + wrap-exceptions: true + # 是否将字符数组输出为数组 + write-char-arrays-as-json-arrays: true + # 格式化输出(加入空格/回车) + indent-output: false + # 对 Map 类型按键排序 + order-map-entries-by-keys: true + # 是否将日期/时间序列化为时间错 + write-dates-as-timestamps: false + write-bigdecimal-as-plain: true + deserialization: + # 未知属性是否报错 + fail-on-unknown-properties: false + generator: + # 是否忽略位置属性 + ignore-unknown: true + visibility: + creator: public_only + field: any + getter: public_only + is-getter: public_only + setter: public_only + + datasource: + hikari: + minimum-idle: 10 + maximum-pool-size: 30 + auto-commit: true + idle-timeout: 30000 + pool-name: HikariCP + max-lifetime: 900000 + connection-timeout: 10000 + connection-test-query: SELECT 1 + validation-timeout: 1000 +logging: + level: + com.njzscloud.common.sichen.mapper: off + com.njzscloud: debug + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + configuration: + default-enum-type-handler: com.njzscloud.common.mp.support.handler.e.EnumTypeHandlerDealer diff --git a/njzscloud-svr/src/main/resources/logback-spring.xml b/njzscloud-svr/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b1fc62f --- /dev/null +++ b/njzscloud-svr/src/main/resources/logback-spring.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + false + + ${console_pattern} + + + + + ${log_path}/${service_name}.log + + ${log_path}/%d{yyyy-MM, aux}/${service_name}.%d{yyyy-MM-dd}.%i.log.zip + 50MB + 15 + + + ${file_pattern} + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/auth/AuthMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/auth/AuthMapper.xml new file mode 100644 index 0000000..876714f --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/auth/AuthMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/endpoint/EndpointMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/endpoint/EndpointMapper.xml new file mode 100644 index 0000000..842153c --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/endpoint/EndpointMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/menu/MenuMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/menu/MenuMapper.xml new file mode 100644 index 0000000..05da52d --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/menu/MenuMapper.xml @@ -0,0 +1,6 @@ + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/resource/ResourceMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/resource/ResourceMapper.xml new file mode 100644 index 0000000..8c43d39 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/resource/ResourceMapper.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/role/RoleMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/role/RoleMapper.xml new file mode 100644 index 0000000..edeeb70 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/role/RoleMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/task/TaskMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/task/TaskMapper.xml new file mode 100644 index 0000000..88080f7 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/task/TaskMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/user/SysUserRoleMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/user/SysUserRoleMapper.xml new file mode 100644 index 0000000..142ab05 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/user/SysUserRoleMapper.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/njzscloud-svr/src/main/resources/mapper/sys/user/UserMapper.xml b/njzscloud-svr/src/main/resources/mapper/sys/user/UserMapper.xml new file mode 100644 index 0000000..eb5e1f4 --- /dev/null +++ b/njzscloud-svr/src/main/resources/mapper/sys/user/UserMapper.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + diff --git a/njzscloud-svr/src/test/java/a/A.java b/njzscloud-svr/src/test/java/a/A.java new file mode 100644 index 0000000..3e8b4f5 --- /dev/null +++ b/njzscloud-svr/src/test/java/a/A.java @@ -0,0 +1,24 @@ +package a; + +import cn.hutool.core.img.FontUtil; +import cn.hutool.core.img.Img; +import cn.hutool.core.img.ImgUtil; + +import java.awt.*; +import java.io.File; + +public class A { + public static void main(String[] args) { + long x = System.currentTimeMillis(); + + Font font = FontUtil.createSansSerifFont(16); + + Img img = Img.from(new File("D:\\W\\1.jpg")) + .setPositionBaseCentre(false) + .pressText("宇宙星系桌面壁纸", Color.WHITE, font, 0, 100, 0.5f) + .pressText("宇宙星系桌面壁纸", Color.WHITE, font, 0, 200, 0.5f); + ImgUtil.write(img.getImg(), new File("D:\\W\\2.jpg")); + + System.out.println(System.currentTimeMillis() - x); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..78265b4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,232 @@ + + + 4.0.0 + + com.njzscloud + njzscloud-dispose + 0.0.1 + pom + + + njzscloud-common + njzscloud-svr + + + + 21 + 21 + true + UTF-8 + + 3.3.1 + + 3.5.7 + + 3.5.14 + + 2.0.51 + 4.0.3 + 5.8.41 + 3.3.0 + 4.12.0 + 8.6.0 + + + + + + com.njzscloud + njzscloud-common-core + 0.0.1 + + + com.njzscloud + njzscloud-common-ws + 0.0.1 + + + com.njzscloud + njzscloud-common-sshtunnel + 0.0.1 + + + com.njzscloud + njzscloud-common-sichen + 0.0.1 + + + com.njzscloud + njzscloud-common-wechat + 0.0.1 + + + com.njzscloud + njzscloud-common-http + 0.0.1 + + + com.njzscloud + njzscloud-common-sn + 0.0.1 + + + com.njzscloud + njzscloud-common-mp + 0.0.1 + + + com.njzscloud + njzscloud-common-mvc + 0.0.1 + + + com.njzscloud + njzscloud-common-redis + 0.0.1 + + + com.njzscloud + njzscloud-common-oss + 0.0.1 + + + com.njzscloud + njzscloud-common-security + 0.0.1 + + + com.njzscloud + njzscloud-common-gen + 0.0.1 + + + com.njzscloud + njzscloud-common-mqtt + 0.0.1 + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + io.minio + minio + ${minio.version} + + + cglib + cglib + ${cglib.version} + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + org.projectlombok + lombok + 1.18.30 + + + cn.hutool + hutool-bom + ${hutool.version} + pom + import + + + com.baomidou + mybatis-plus-bom + ${mybatis-plus.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + ${project.name}-${project.version} + + + src/main/resources + true + + cert/** + + + + src/main/resources + false + + cert/** + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + UTF-8 + + pdf + ico + eot + ttf + woff + woff2 + ftl + css + svg + js + otf + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + + diff --git a/z-doc/pdma/njzscloud-dispose.pdma b/z-doc/pdma/njzscloud-dispose.pdma index a9a2527..a74c5c9 100644 --- a/z-doc/pdma/njzscloud-dispose.pdma +++ b/z-doc/pdma/njzscloud-dispose.pdma @@ -11,7 +11,7 @@ "driverFiles": "mysql-connector-j-8.3.0.jar", "jdbcReferDriver": "com.mysql.cj.jdbc.Driver", "jdbcReferUrl": "jdbc:mysql://[ip]:[port]/[dbname]?characterEncoding=UTF-8&useSSL=false&useUnicode=true&serverTimezone=UTC", - "tableCreate": "DROP TABLE IF EXISTS {{=it.defKey}};\r\nCREATE TABLE {{=it.defKey}}\r\n(\r\n{{ pkList = [] ; }}\r\n{{~it.fields:field:index}}\r\n {{? field.primaryKey }}{{ pkList.push(field.defKey) }}{{?}}\r\n {{=field.defKey}} {{=field.dbDataType}}{{?field.dataLen>0}}{{='('}}{{=field.dataLen}}{{?field.numScale>0}}{{=','}}{{=field.numScale}}{{?}}{{=')'}}{{?}} {{= field.defaultValue ? 'DEFAULT' + ' ' + field.defaultValue : '' }} {{= field.notNull ? 'NOT NULL' : 'NULL' }} {{= field.autoIncrement ? 'AUTO_INCREMENT' : '' }} COMMENT '{{=field.intro.length > 0? field.defName + ';' + field.intro : field.defName}}'{{= index < it.fields.length-1 ? ',' : ( pkList.length>0 ? ',' :'' ) }}{{~}}\r\n{{? pkList.length >0 }}\r\n PRIMARY KEY ({{~pkList:pkName:i}}{{= pkName }}{{= i0}}{{='('}}{{=field.dataLen}}{{?field.numScale>0}}{{=','}}{{=field.numScale}}{{?}}{{=')'}}{{?}} {{= field.defaultValue ? 'DEFAULT' + ' ' + field.defaultValue : '' }} {{= field.notNull ? 'NOT NULL' : 'NULL' }} {{= field.autoIncrement ? 'AUTO_INCREMENT' : '' }} COMMENT '{{=field.intro ? field.defName + ';' + field.intro : field.defName}}'{{= index < it.fields.length-1 ? ',' : ( pkList.length>0 ? ',' :'' ) }}{{~}}\r\n{{? pkList.length >0 }}\r\n PRIMARY KEY ({{~pkList:pkName:i}}{{= pkName }}{{= i 0}}\nALTER TABLE {{= tableKey }} ADD PRIMARY KEY ({{~indexFields:field:i}}`{{= field.defKey }}`{{= i < indexFields.length-1 ? ',' : '' }}{{~}});\n{{?}}\n{{?}}\n{{? updateKeys.some((item) => item === 'defName' || item === 'intro') }}ALTER TABLE {{= tableKey }} COMMENT '{{=it.func.strJoin(defName, intro, \";\", true)}}'; {{?}}\n{{? updateKeys.some((item) => item === 'defKey') }}ALTER TABLE {{= schemaName }}{{=it.defKey}} RENAME TO {{= schemaName }}{{=defKey}};{{?}}\n", "tableDelete": "{{\n let schemaName = it.schemaName ? `${it.schemaName}.` : '';\n const tableKey = `${schemaName}${it.defKey}`;\n}}\nDROP TABLE IF EXISTS {{= tableKey }};", "columnCreate": "{{\r\n let fieldsUpdate = it.fieldsUpdate;\r\n const computeLenAndNum = (dataLen, numScale, maxDataLen, maxNumScale) => {\r\n if(!dataLen || dataLen <= 0) {\r\n return '';\r\n }\r\n let currentDataLen = dataLen, currentNumScale = numScale;\r\n if(dataLen >= maxDataLen) {\r\n currentDataLen = maxDataLen;\r\n }\r\n if(!numScale || numScale <= 0) {\r\n return `(${currentDataLen},0)`;\r\n }\r\n if(currentNumScale > maxNumScale) {\r\n currentNumScale = maxNumScale\r\n }\r\n if(currentNumScale > currentDataLen && currentDataLen <= maxNumScale) {\r\n currentNumScale = currentDataLen;\r\n }\r\n return `(${currentDataLen},${currentNumScale})`;\r\n };\r\n const computeDefaultValue = (field) => {\r\n const { defaultValue, dbDataType, primaryKey } = field;\r\n if(!defaultValue) {\r\n return '';\r\n }\r\n if(dbDataType.toUpperCase().endsWith('BLOB') || \r\n dbDataType.toUpperCase().endsWith('GEOMETRY') || \r\n dbDataType.toUpperCase().endsWith('TEXT') || \r\n dbDataType.toUpperCase().endsWith('JSON')) {\r\n return '';\r\n } else if(dbDataType.toUpperCase() === 'ENUM' ||\r\n dbDataType.toUpperCase() === 'SET') {\r\n return ` DEFAULT \"${defaultValue}\"`;\r\n } else {\r\n return ` DEFAULT ${defaultValue}`;\r\n }\r\n };\r\n const computeDatatype = (field) => {\r\n const { dbDataType, dataLen, numScale, defaultValue } = field;\r\n if(!dbDataType) {\r\n return '';\r\n }\r\n if(dbDataType.toUpperCase() === 'VARCHAR' ||\r\n dbDataType.toUpperCase() === 'NVARCHAR' ||\r\n dbDataType.toUpperCase() === 'VARBINARY') {\r\n return `${dbDataType}(${dataLen ? dataLen : 128})`;\r\n } else if(dbDataType.toUpperCase() === 'DATE' ||\r\n dbDataType.toUpperCase() === 'YEAR' ||\r\n dbDataType.toUpperCase() === 'TINYTEXT' ||\r\n dbDataType.toUpperCase() === 'MEDIUMTEXT' ||\r\n dbDataType.toUpperCase() === 'LONGTEXT' ||\r\n dbDataType.toUpperCase() === 'TINYBLOB' ||\r\n dbDataType.toUpperCase() === 'MEDIUMBLOB' ||\r\n dbDataType.toUpperCase() === 'LONGBLOB' ||\r\n dbDataType.toUpperCase() === 'BOOLEAN' ||\r\n dbDataType.toUpperCase() === 'FLOAT' ||\r\n dbDataType.toUpperCase() === 'INT' ||\r\n dbDataType.toUpperCase() === 'JSON' ) {\r\n return dbDataType;\r\n } else if(dbDataType.toUpperCase() === 'ENUM' ||\r\n dbDataType.toUpperCase() === 'SET' ) {\r\n return `${dbDataType}(${defaultValue})`;\r\n } else if(dbDataType.toUpperCase() === 'TIME' ||\r\n dbDataType.toUpperCase() === 'DATETIME' ||\r\n dbDataType.toUpperCase() === 'TIMESTAMP') {\r\n return `${dbDataType}${(dataLen && dataLen >= 0 && dataLen <= 6) ? `(${dataLen})` : ''}`;\r\n } else if(dbDataType.toUpperCase() === 'DOUBLE') {\r\n return `${dbDataType}${computeLenAndNum(dataLen, numScale, 255, 30)}`;\r\n } else if(dbDataType.toUpperCase() === 'DECIMAL') {\r\n return `${dbDataType}${computeLenAndNum(dataLen, numScale, 65, 30)}`;\r\n } else if(dataLen && dataLen > 0) {\r\n return `${dbDataType}(${dataLen})`;\r\n }\r\n return `${dbDataType}`;\r\n };\r\n let schemaName = it.schemaName ? `${it.schemaName}.` : '';\r\n const tableKey = `${schemaName}${it.defKey}`;\r\n}}\r\n{{~fieldsUpdate:field:index}}\r\nALTER TABLE {{= tableKey }} ADD COLUMN `{{=field.defKey}}` {{=computeDatatype(field)}} {{= field.notNull ? 'NOT NULL' : '' }}{{= field.autoIncrement ? ' AUTO_INCREMENT ' : '' }}{{= computeDefaultValue(field)}} {{? field.defName || field.intro }} COMMENT '{{=it.func.strJoin(field.defName, field.intro, \";\", true)}}'{{?}};\r\n{{~}}\r\n", @@ -563,7 +563,7 @@ "defName": null, "icon": "\n \n \n \n \n ", "color": "", - "orderValue": 1, + "orderValue": 0, "isEnabled": 1, "id": "BPP6L7ZLM7EAB", "codegens": [ @@ -599,6 +599,42 @@ } ] }, + { + "defKey": "NodeJS", + "defName": null, + "icon": "\n \n \n \n \n ", + "color": "", + "orderValue": 1, + "isEnabled": 1, + "id": "BPP6MKTVM7EAB", + "codegens": [ + { + "genKey": "Ts", + "genItems": [ + { + "itemKey": "page.vue", + "itemTemplate": "{{\r\nlet first_ =it.defKey.indexOf('_');\r\nlet moduleName = it.defKey.slice(0,first_);\r\nlet pageName = it.defKey.slice(first_ +1,first_ +2).toUpperCase() + it.defKey.slice(first_ +2);\r\nlet pageName_ = it.defKey.slice(first_ +1);\r\nlet fields = it.fields.filter(it=>it.defKey!=='id' && it.defKey!=='creator_id' && it.defKey!=='modifier_id' && it.defKey!=='create_time' && it.defKey!=='modify_time' && it.defKey!=='deleted');\r\n}}\r\n\r\n\r\n\r\n\r\n$blankline" + }, + { + "itemKey": "page.ts", + "itemTemplate": "{{\r\nlet first_ =it.defKey.indexOf('_');\r\nlet moduleName = it.defKey.slice(0,first_);\r\nlet pageName = it.defKey.slice(first_ +1,first_ +2).toUpperCase() + it.defKey.slice(first_ +2);\r\nlet pageName_ = it.defKey.slice(first_ +1);\r\n}}export default {\r\n component: () => import('@/pages/{{=moduleName}}/{{=pageName_}}/{{=pageName}}.vue'),\r\n} as RouterTypes.RouteConfig\r\n$blankline" + }, + { + "itemKey": "form.vue", + "itemTemplate": "{{\r\nlet first_ =it.defKey.indexOf('_');\r\nlet moduleName = it.defKey.slice(0,first_);\r\nlet pageName = it.defKey.slice(first_ +1,first_ +2).toUpperCase() + it.defKey.slice(first_ +2);\r\nlet pageName_ = it.defKey.slice(first_ +1);\r\nlet fields = it.fields.filter(it=>it.defKey!=='id' && it.defKey!=='creator_id' && it.defKey!=='modifier_id' && it.defKey!=='create_time' && it.defKey!=='modify_time' && it.defKey!=='deleted');\r\n}}\r\n\r\n\r\n\r\n\r\n$blankline" + }, + { + "itemKey": "api.ts", + "itemTemplate": "{{\r\nlet first_ =it.defKey.indexOf('_');\r\nlet moduleName = it.defKey.slice(0,first_);\r\nlet pageName = it.defKey.slice(first_ +1,first_ +2).toUpperCase() + it.defKey.slice(first_ +2);\r\nlet pageName_ = it.defKey.slice(first_ +1);\r\n}}import {\r\n get,\r\n post\r\n} from '@/common/utils/http-util.ts'\r\n\r\nexport default {\r\n paging(data: {{=pageName}}Types.Search{{=pageName}}Param) {\r\n return get>('/{{=it.defKey}}/paging', data)\r\n },\r\n detail(id: string) {\r\n return get<{{=pageName}}Types.Search{{=pageName}}Result>('/{{=it.defKey}}/detail', {id})\r\n },\r\n add(data: {{=pageName}}Types.Add{{=pageName}}Param) {\r\n return post('/{{=it.defKey}}/add', data)\r\n },\r\n modify(data: {{=pageName}}Types.Modify{{=pageName}}Param) {\r\n return post('/{{=it.defKey}}/modify', data)\r\n },\r\n del(ids: string[]) {\r\n return post('/{{=it.defKey}}/del', ids)\r\n },\r\n}\r\n$blankline" + }, + { + "itemKey": "d.ts", + "itemTemplate": "{{\r\nlet first_ =it.defKey.indexOf('_');\r\nlet moduleName = it.defKey.slice(0,first_);\r\nlet pageName = it.defKey.slice(first_ +1,first_ +2).toUpperCase() + it.defKey.slice(first_ +2);\r\nlet fields = it.fields.filter(it=>it.defKey!=='creator_id' && it.defKey!=='modifier_id' && it.defKey!=='create_time' && it.defKey!=='modify_time' && it.defKey!=='deleted');\r\n}}export {}\r\n\r\ndeclare global {\r\n namespace {{=pageName}}Types {\r\n interface Search{{=pageName}}Param extends G.PageParam {\r\n {{~fields:field:index}}\r\n // {{=field.defName}}{{? field.comment }}; {{=field.comment}}{{?}}\r\n {{=it.func.camel(field.defKey, false)}}?: {{=field.dbDataType === 'TINYINT' && field.dataLen === 1 ? 'boolean' : field.langDataType}}\r\n {{~}}\r\n }\r\n \r\n interface Search{{=pageName}}Result {\r\n {{~fields:field:index}}\r\n // {{=field.defName}}{{? field.comment }}; {{=field.comment}}{{?}}\r\n {{=it.func.camel(field.defKey, false)}}?: {{=field.dbDataType === 'TINYINT' && field.dataLen === 1 ? 'boolean' : field.langDataType}}\r\n {{~}}\r\n }\r\n \r\n interface Add{{=pageName}}Param {\r\n {{~fields:field:index}}\r\n // {{=field.defName}}{{? field.comment }}; {{=field.comment}}{{?}}\r\n {{=it.func.camel(field.defKey, false)}}?: {{=field.dbDataType === 'TINYINT' && field.dataLen === 1 ? 'boolean' : field.langDataType}}\r\n {{~}} \r\n }\r\n \r\n interface Modify{{=pageName}}Param {\r\n {{~fields:field:index}}\r\n // {{=field.defName}}{{? field.comment }}; {{=field.comment}}{{?}}\r\n {{=it.func.camel(field.defKey, false)}}?: {{=field.dbDataType === 'TINYINT' && field.dataLen === 1 ? 'boolean' : field.langDataType}}\r\n {{~}}\r\n }\r\n }\r\n}\r\n$blankline" + } + ] + } + ] + }, { "defKey": "Rust", "defName": null, @@ -665,34 +701,12 @@ } ] }, - { - "defKey": "NodeJS", - "defName": null, - "icon": "\n \n \n \n \n ", - "color": "", - "orderValue": 5, - "isEnabled": 0, - "id": "BPP6MKTVM7EAB", - "codegens": [ - { - "genKey": "default", - "genIntro": null, - "genItems": [ - { - "itemKey": "content", - "itemIntro": null, - "itemTemplate": "{{\r\n var today=new Date();\r\n var fullYear=today.getFullYear();\r\n var month=today.getMonth() + 1;\r\n var days=today.getDate();\r\n}}\r\n/*\r\n * @author : \r\n * @date : {{=fullYear}}-{{=month}}-{{=days}}\r\n * @desc : {{=it.func.join(it.defName,it.comment,'-')}}\r\n */\r\nclass {{=it.func.camel(it.defKey, true)}} {\r\n constructor() {\r\n {{~it.fields:field:index}}\r\n this._{{=it.func.camel(field.defKey, true)}} = null;\r\n {{~}}\r\n }\r\n\r\n {{~it.fields:field:index}}\r\n /**\r\n * {{=it.func.join(field.defName, field.comment, ';')}}\r\n */\r\n get {{=it.func.camel(field.defKey, true)}}() {\r\n return this._{{=it.func.camel(field.defKey, true)}};\r\n }\r\n\r\n set {{=it.func.camel(field.defKey, true)}}(value) {\r\n this._{{=it.func.camel(field.defKey, true)}} = value;\r\n }\r\n {{~}}\r\n\r\n toString() {\r\n return JSON.stringify(this);\r\n }\r\n}\r\n" - } - ] - } - ] - }, { "defKey": "Python", "defName": null, "icon": "\n \n \n \n \n ", "color": "", - "orderValue": 6, + "orderValue": 5, "isEnabled": 0, "id": "BPP6MLTRQ7EAC", "codegens": [ @@ -714,7 +728,7 @@ "defName": null, "icon": "\n \n \n \n \n ", "color": "", - "orderValue": 7, + "orderValue": 6, "isEnabled": 0, "id": "BPP6MMSQQ7EAB", "codegens": [ @@ -736,7 +750,7 @@ "defName": null, "icon": "\n \n \n \n \n ", "color": "", - "orderValue": 8, + "orderValue": 7, "isEnabled": 0, "id": "BPP6MO4ZM7EAB", "codegens": [ @@ -763,7 +777,7 @@ "defName": null, "icon": "\n \n \n \n \n ", "color": "", - "orderValue": 9, + "orderValue": 8, "isEnabled": 0, "id": "BPP6MQB6M7EAB", "codegens": [ @@ -829,9 +843,9 @@ "Swift": "String", "testa": "char5", "GoLang": "string", - "NodeJS": "string", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } }, { @@ -881,9 +895,9 @@ "Swift": "String", "testa": "varchar", "GoLang": "string", - "NodeJS": "String", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } }, { @@ -892,7 +906,7 @@ "defName": "TINYINT", "icon": "", "color": null, - "requireLen": 0, + "requireLen": 1, "requireScale": 0, "lenMax": "0", "often": "9", @@ -933,9 +947,9 @@ "Swift": "Int8", "testa": "tiniyint", "GoLang": "int8", - "NodeJS": "Number", "Python": "int", - "Java": "Byte" + "Java": "Byte", + "NodeJS": "number" } }, { @@ -985,9 +999,9 @@ "Swift": "Int16", "testa": "smallint", "GoLang": "int16", - "NodeJS": "Number", "Python": "int", - "Java": "Short" + "Java": "Short", + "NodeJS": "number" } }, { @@ -1036,9 +1050,9 @@ "Swift": "Int32", "testa": "mediumint", "GoLang": "int32", - "NodeJS": "Number", "Python": "int", - "Java": "Integer" + "Java": "Integer", + "NodeJS": "number" } }, { @@ -1088,9 +1102,9 @@ "Swift": "Int", "testa": "int", "GoLang": "int", - "NodeJS": "Number", "Python": "int", - "Java": "Integer" + "Java": "Integer", + "NodeJS": "number" } }, { @@ -1139,9 +1153,9 @@ "Swift": "Int64", "testa": "b", "GoLang": "int64", - "NodeJS": "BigInt", "Python": "int", - "Java": "Long" + "Java": "Long", + "NodeJS": "string" } }, { @@ -1190,9 +1204,9 @@ "Swift": "Float", "testa": "", "GoLang": "float32", - "NodeJS": "Number", "Python": "float", - "Java": "Float" + "Java": "Float", + "NodeJS": "number" } }, { @@ -1241,9 +1255,9 @@ "Swift": "Double", "testa": "", "GoLang": "float64", - "NodeJS": "Number", "Python": "float", - "Java": "Double" + "Java": "Double", + "NodeJS": "number" } }, { @@ -1292,9 +1306,9 @@ "Swift": "Decimal", "testa": "", "GoLang": "big.Float", - "NodeJS": "Number", "Python": "decimal.Decimal", - "Java": "BigDecimal" + "Java": "BigDecimal", + "NodeJS": "number" } }, { @@ -1343,9 +1357,9 @@ "Swift": "Date", "testa": "", "GoLang": "time.Duration", - "NodeJS": "Date", "Python": "datetime.time", - "Java": "LocalTime" + "Java": "LocalTime", + "NodeJS": "string" } }, { @@ -1394,9 +1408,9 @@ "Swift": "Date", "testa": "", "GoLang": "time.Time", - "NodeJS": "Date", "Python": "datetime.date", - "Java": "LocalDate" + "Java": "LocalDate", + "NodeJS": "string" } }, { @@ -1445,9 +1459,9 @@ "Swift": "Date", "testa": "", "GoLang": "time.Time", - "NodeJS": "Date", "Python": "datetime.datetime", - "Java": "LocalDateTime" + "Java": "LocalDateTime", + "NodeJS": "string" } }, { @@ -1496,9 +1510,9 @@ "Swift": "String", "testa": "", "GoLang": "string", - "NodeJS": "string", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } }, { @@ -1547,9 +1561,9 @@ "Swift": "String", "testa": "", "GoLang": "string", - "NodeJS": "string", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } }, { @@ -1598,9 +1612,9 @@ "Swift": "String", "testa": "", "GoLang": "string", - "NodeJS": "string", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } }, { @@ -1649,9 +1663,9 @@ "Swift": "String", "testa": "", "GoLang": "string", - "NodeJS": "string", "Python": "str", - "Java": "String" + "Java": "String", + "NodeJS": "string" } } ], @@ -2902,12 +2916,12 @@ "usedFor": "ALL_IN_ONE", "dbDialectKey": "MySQL", "driverFiles": "mysql-connector-j-8.3.0.jar", - "extParams": "schemaName=njzscloud_chu", + "extParams": "schemaName=njzscloud_dispose", "password": "ZXcgoJqNVcVNLolCmBKQ2A==", "id": "FCD5D2AF-36D6-4ED7-AAC2-C5F5FB02332A", "username": "xa1HO8gevrFNnWjL9Bsgnw==", "driverClassName": "com.mysql.cj.jdbc.Driver", - "url": "YJUIuHpIHkliSUlYOgt+kiXh6RwxUNQlw+q6q7GdYJWfSIlGT6vl9O/L8CBqwS0isOJT4ceQGCDwOXtUNK973MyxN/BcoC56AywnBxQ+4B9otJjgCDXZUIOT+D2p8VDZJvheD39FwaCDvgB8Sqk2s/WTiPkVUWys0eeV5PwGzzI=", + "url": "YJUIuHpIHkliSUlYOgt+kiXh6RwxUNQlw+q6q7GdYJXXXCk8rRlwALc95LX3iaP0BgmWTaMRbzM6hxOhqoB+KhvvYpNrizn8z9zAI+f/XEu48YMFNr8If4qtwZf0hGwZu51kcEmQ3Jr2OindlF6c3cr3UejpfyLtNpwJdeWJNtYNwb3Z9eSe8k9yfNl4Y64p", "envId": "572377B5-884E-47DB-835F-6326AB9561C7" } ], @@ -3020,104 +3034,132 @@ "orderValue": 2 }, { - "refObjectId": "0CE38F31-EFD9-43A6-9BA5-3E13696C8444", - "refObjectType": "E", + "refObjectId": "FEF038F4-4AEE-4930-9C4B-E08E7F4F3A23", + "refObjectType": "P", "orderValue": 3 }, { - "refObjectId": "18C34285-97B3-48BD-BF1C-80FFD204878F", + "refObjectId": "0CE38F31-EFD9-43A6-9BA5-3E13696C8444", "refObjectType": "E", "orderValue": 4 }, { - "refObjectId": "CA27E812-84DF-415B-A517-D8EE72A5851F", + "refObjectId": "18C34285-97B3-48BD-BF1C-80FFD204878F", "refObjectType": "E", "orderValue": 5 }, { - "refObjectId": "66210A03-E5D3-4AE3-9DF5-0A5FF1B55A2B", + "refObjectId": "CA27E812-84DF-415B-A517-D8EE72A5851F", "refObjectType": "E", "orderValue": 6 }, { - "refObjectId": "298B913C-A7EC-42CF-B869-6F8F3EEA5CEB", + "refObjectId": "66210A03-E5D3-4AE3-9DF5-0A5FF1B55A2B", "refObjectType": "E", "orderValue": 7 }, { - "refObjectId": "9B28F97D-A220-48BA-B256-99952BD84550", + "refObjectId": "298B913C-A7EC-42CF-B869-6F8F3EEA5CEB", "refObjectType": "E", "orderValue": 8 }, { - "refObjectId": "1F8CC750-13F7-406D-9667-C328491C1A6E", + "refObjectId": "9B28F97D-A220-48BA-B256-99952BD84550", "refObjectType": "E", "orderValue": 9 }, { - "refObjectId": "778D9EC8-362D-438D-8522-7DC1A9C4D55F", + "refObjectId": "1F8CC750-13F7-406D-9667-C328491C1A6E", "refObjectType": "E", "orderValue": 10 }, { - "refObjectId": "A0E6ACDD-B2DA-478D-B98F-ECEF0B624979", + "refObjectId": "778D9EC8-362D-438D-8522-7DC1A9C4D55F", "refObjectType": "E", "orderValue": 11 }, { - "refObjectId": "4C319989-B031-4A47-872B-632383A686EE", + "refObjectId": "A0E6ACDD-B2DA-478D-B98F-ECEF0B624979", "refObjectType": "E", "orderValue": 12 }, { - "refObjectId": "C6C96F4E-DDF2-4F8E-ABE9-3DFA6BE487C5", + "refObjectId": "4C319989-B031-4A47-872B-632383A686EE", "refObjectType": "E", "orderValue": 13 }, { - "refObjectId": "6AE16DDA-CDCA-4E66-80B5-775ACF4B124E", + "refObjectId": "C6C96F4E-DDF2-4F8E-ABE9-3DFA6BE487C5", "refObjectType": "E", "orderValue": 14 }, { - "refObjectId": "457D3BF0-A5A6-4636-A83E-3903ACD2C4A2", + "refObjectId": "6AE16DDA-CDCA-4E66-80B5-775ACF4B124E", "refObjectType": "E", "orderValue": 15 }, { - "refObjectId": "4DB0E9C1-05E9-47ED-A7AA-6BC7E87856D7", + "refObjectId": "457D3BF0-A5A6-4636-A83E-3903ACD2C4A2", "refObjectType": "E", "orderValue": 16 }, { - "refObjectId": "96FF7EC9-5753-46BC-AE85-E2BDFEE6E043", + "refObjectId": "4DB0E9C1-05E9-47ED-A7AA-6BC7E87856D7", "refObjectType": "E", "orderValue": 17 }, { - "refObjectId": "B724EF27-B827-4067-8F8F-43FD73F9DE24", + "refObjectId": "96FF7EC9-5753-46BC-AE85-E2BDFEE6E043", "refObjectType": "E", "orderValue": 18 }, { - "refObjectId": "BEC9C790-0D4F-477F-9F31-8A9F41A4977D", + "refObjectId": "B724EF27-B827-4067-8F8F-43FD73F9DE24", "refObjectType": "E", "orderValue": 19 }, + { + "refObjectId": "BEC9C790-0D4F-477F-9F31-8A9F41A4977D", + "refObjectType": "E", + "orderValue": 20 + }, { "refObjectId": "7F405312-ACDB-41FB-A8E0-6BD9CECA52D3", "refObjectType": "P", - "orderValue": 20 + "orderValue": 21 }, { "refObjectId": "9433E723-2A22-4004-8EEC-EA148772C5D4", "refObjectType": "P", - "orderValue": 21 + "orderValue": 22 } ], "diagramRefs": [], "peerOrder": 1 }, + { + "id": "76F28967-EDEA-4FA2-9E00-FC0A14C8D915", + "defKey": "mfg", + "defName": "生产", + "classifyType": "MANUAL", + "manualClassify": "[\"P\"]", + "bindSchema": 0, + "children": null, + "entityRefs": [ + { + "refObjectId": "0B223F50-E8AF-40F4-8444-1B22D1171407", + "refObjectType": "P", + "orderValue": 1 + }, + { + "refObjectId": "8597F6A3-0AB9-4885-BC3A-72C3ACC4B756", + "refObjectType": "P", + "orderValue": 2 + } + ], + "diagramRefs": [], + "peerOrder": 2 + }, { "id": "C2ED7A76-B66C-4323-996C-0EB696D22C68", "defKey": "goods", @@ -3139,25 +3181,126 @@ } ], "diagramRefs": [], - "peerOrder": 2 + "peerOrder": 3 }, { "id": "8FF17ADC-6C66-4DBD-B7FC-0C0B6E35A910", - "defKey": "cost", - "defName": "费用", + "defKey": "fin", + "defName": "财务", "classifyType": "MANUAL", "manualClassify": "[\"P\"]", "bindSchema": 0, "children": null, "entityRefs": [ { - "refObjectId": "1CD9AEA6-DAB8-40DF-8318-BCA343176C13", + "refObjectId": "31EE5227-5597-42B8-8D7A-B558F708C076", "refObjectType": "P", "orderValue": 1 + }, + { + "refObjectId": "3B83C23F-D49C-4D67-830C-882D91A57927", + "refObjectType": "P", + "orderValue": 2 + }, + { + "refObjectId": "EDDB991D-570B-493F-AF67-97787AC9BAFC", + "refObjectType": "P", + "orderValue": 3 + }, + { + "refObjectId": "48C64C11-5A89-4DDD-95A6-3EBF40F399FB", + "refObjectType": "P", + "orderValue": 4 + }, + { + "refObjectId": "4476CB18-4B15-486E-A0AD-DA49AB267305", + "refObjectType": "P", + "orderValue": 5 } ], "diagramRefs": [], - "peerOrder": 3 + "peerOrder": 4 + }, + { + "id": "47A6C9D4-056A-48B0-B6B2-4BF5AC78D0E3", + "defKey": "wh", + "defName": "仓储", + "classifyType": "MANUAL", + "manualClassify": "[\"P\"]", + "bindSchema": 0, + "children": null, + "entityRefs": [ + { + "refObjectId": "9645DE46-7F49-4C24-99F2-9C8D88C5B328", + "refObjectType": "P", + "orderValue": 1 + }, + { + "refObjectId": "04B05244-7665-40E6-9A98-BDB20746A886", + "refObjectType": "P", + "orderValue": 2 + }, + { + "refObjectId": "153541F6-C971-4324-B97C-BEF689627B24", + "refObjectType": "P", + "orderValue": 3 + }, + { + "refObjectId": "C9A36E0B-ECA6-4D3C-AEE5-691F607359F6", + "refObjectType": "P", + "orderValue": 4 + } + ], + "diagramRefs": [], + "peerOrder": 5 + }, + { + "id": "33C5A65F-6DAF-4CE2-9E03-E170AA482B8F", + "defKey": "cst", + "defName": "收/销/运", + "classifyType": "MANUAL", + "manualClassify": "[\"P\"]", + "bindSchema": 0, + "children": null, + "entityRefs": [ + { + "refObjectId": "1A2D551C-214D-469D-B0C5-917C29D725B2", + "refObjectType": "P", + "orderValue": 1 + }, + { + "refObjectId": "DECB0762-9BE3-429A-81F7-891B7C960B04", + "refObjectType": "P", + "orderValue": 2 + }, + { + "refObjectId": "DF643AC9-1A78-408B-9F20-9C659BC475FA", + "refObjectType": "P", + "orderValue": 3 + }, + { + "refObjectId": "718C57B0-4799-48E1-8651-57527A3BFF8B", + "refObjectType": "P", + "orderValue": 4 + }, + { + "refObjectId": "F13139EC-3554-4C1E-8C09-1A6C238C539A", + "refObjectType": "P", + "orderValue": 5 + }, + { + "refObjectId": "38B910DC-4D79-448E-9EA4-CED1E92B03DE", + "refObjectType": "P", + "orderValue": 6 + }, + { + "refObjectId": "F573B178-BE6E-48AB-8AF0-6121E7320EB9", + "refObjectType": "P", + "orderValue": 7 + } + ], + "diagramRefs": [], + "peerOrder": 6 } ], "flat": { @@ -3278,9 +3421,99 @@ "orderValue": 23 }, { - "refObjectId": "1CD9AEA6-DAB8-40DF-8318-BCA343176C13", + "refObjectId": "FEF038F4-4AEE-4930-9C4B-E08E7F4F3A23", "refObjectType": "P", "orderValue": 24 + }, + { + "refObjectId": "9645DE46-7F49-4C24-99F2-9C8D88C5B328", + "refObjectType": "P", + "orderValue": 25 + }, + { + "refObjectId": "04B05244-7665-40E6-9A98-BDB20746A886", + "refObjectType": "P", + "orderValue": 26 + }, + { + "refObjectId": "153541F6-C971-4324-B97C-BEF689627B24", + "refObjectType": "P", + "orderValue": 27 + }, + { + "refObjectId": "C9A36E0B-ECA6-4D3C-AEE5-691F607359F6", + "refObjectType": "P", + "orderValue": 28 + }, + { + "refObjectId": "3B83C23F-D49C-4D67-830C-882D91A57927", + "refObjectType": "P", + "orderValue": 29 + }, + { + "refObjectId": "EDDB991D-570B-493F-AF67-97787AC9BAFC", + "refObjectType": "P", + "orderValue": 30 + }, + { + "refObjectId": "48C64C11-5A89-4DDD-95A6-3EBF40F399FB", + "refObjectType": "P", + "orderValue": 31 + }, + { + "refObjectId": "4476CB18-4B15-486E-A0AD-DA49AB267305", + "refObjectType": "P", + "orderValue": 32 + }, + { + "refObjectId": "31EE5227-5597-42B8-8D7A-B558F708C076", + "refObjectType": "P", + "orderValue": 33 + }, + { + "refObjectId": "0B223F50-E8AF-40F4-8444-1B22D1171407", + "refObjectType": "P", + "orderValue": 34 + }, + { + "refObjectId": "8597F6A3-0AB9-4885-BC3A-72C3ACC4B756", + "refObjectType": "P", + "orderValue": 35 + }, + { + "refObjectId": "1A2D551C-214D-469D-B0C5-917C29D725B2", + "refObjectType": "P", + "orderValue": 36 + }, + { + "refObjectId": "DECB0762-9BE3-429A-81F7-891B7C960B04", + "refObjectType": "P", + "orderValue": 37 + }, + { + "refObjectId": "718C57B0-4799-48E1-8651-57527A3BFF8B", + "refObjectType": "P", + "orderValue": 38 + }, + { + "refObjectId": "F13139EC-3554-4C1E-8C09-1A6C238C539A", + "refObjectType": "P", + "orderValue": 39 + }, + { + "refObjectId": "38B910DC-4D79-448E-9EA4-CED1E92B03DE", + "refObjectType": "P", + "orderValue": 40 + }, + { + "refObjectId": "F573B178-BE6E-48AB-8AF0-6121E7320EB9", + "refObjectType": "P", + "orderValue": 41 + }, + { + "refObjectId": "DF643AC9-1A78-408B-9F20-9C659BC475FA", + "refObjectType": "P", + "orderValue": 42 } ], "diagramRefs": [] @@ -3716,7 +3949,7 @@ } ], "indexes": [], - "props": "{\"codegen\":{\"Java_MybatisPlus\":{\"root\":\"\",\"nameSpace\":\"com.zsy\",\"baseName\":\"sys\",\"path\":{},\"var\":{}}}}", + "props": "{\"codegen\":{\"Java_MybatisPlus\":{\"root\":\"\",\"nameSpace\":\"com.zsy\",\"baseName\":\"sys\",\"path\":{},\"var\":{}},\"NodeJS_Ts\":{\"root\":\"C:\\\\Users\\\\24955\\\\Desktop\\\\code\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"user\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}", "refers": [] }, { @@ -5102,7 +5335,8 @@ ] } ], - "refers": [] + "refers": [], + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"role\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}" }, { "id": "1F8CC750-13F7-406D-9667-C328491C1A6E", @@ -6345,7 +6579,7 @@ "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": 255, + "dataLen": 10, "numScale": "", "primaryKey": 0, "notNull": 1, @@ -6387,7 +6621,7 @@ "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": 512, + "dataLen": 128, "numScale": "", "primaryKey": 0, "notNull": 1, @@ -7962,7 +8196,7 @@ "primaryKey": 0, "notNull": 1, "autoIncrement": 0, - "defaultValue": "", + "defaultValue": "''", "stndDictId": "", "stndDictKey": "", "stndFieldId": "", @@ -8051,7 +8285,8 @@ ] } ], - "refers": [] + "refers": [], + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"dict\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}" }, { "id": "457D3BF0-A5A6-4636-A83E-3903ACD2C4A2", @@ -8716,7 +8951,7 @@ "primaryKey": 0, "notNull": 1, "autoIncrement": 0, - "defaultValue": "", + "defaultValue": "''", "stndDictId": "", "stndDictKey": "", "stndFieldId": "", @@ -8811,7 +9046,8 @@ ] } ], - "refers": [] + "refers": [], + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"dict-item\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}" }, { "id": "9B28F97D-A220-48BA-B256-99952BD84550", @@ -9445,9 +9681,9 @@ }, { "id": "1752A8C7-C185-4925-91DB-2997B7D162D5", - "defKey": "tpl_category", - "defName": "模板类型", - "intro": " 字典编码:tpl_category", + "defKey": "lang", + "defName": "语言", + "intro": "", "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", @@ -9457,12 +9693,27 @@ "notNull": 1, "autoIncrement": 0, "defaultValue": "", - "stndDictId": "", - "stndDictKey": "", + "stndDictId": "lang", + "stndDictKey": "lang", "stndFieldId": "", "stndFieldKey": "", - "dictFrom": "", - "dictItems": [], + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "java", + "itemName": "java", + "parentKey": "", + "intro": "", + "id": "0EE38793-3351-41C8-8734-5BC07836F213" + }, + { + "itemKey": "ts", + "itemName": "ts", + "parentKey": "", + "intro": "", + "id": "BF795CCF-2921-48B2-9226-0D4F9A25CDB4" + } + ], "attr1": "", "attr2": "", "attr3": "", @@ -9526,52 +9777,11 @@ "attr19": "", "attr20": "", "origin": "IMPORT" - }, - { - "id": "0CF7E522-DC2B-41E0-BCB6-72E016DD6DF3", - "defKey": "model_data", - "defName": "模型数据", - "intro": "", - "baseDataType": "LONGTEXT", - "bizDomainType": "", - "dbDataType": "LONGTEXT", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndDictKey": "", - "stndFieldId": "", - "stndFieldKey": "", - "dictFrom": "", - "dictItems": [], - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "PDManer", - "attr19": "", - "attr20": "", - "origin": "IMPORT" } ], "indexes": [], - "refers": [] + "refers": [], + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"C:\\\\Users\\\\24955\\\\Desktop\\\\code\\\\njzscloud-dispose-web\\\\src\\\\pages\\\\sys\\\\gen\",\"nameSpace\":\"sys\",\"baseName\":\"tpl\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}" }, { "id": "BEC9C790-0D4F-477F-9F31-8A9F41A4977D", @@ -9580,7 +9790,7 @@ "defName": "任务执行日志表", "intro": null, "schemaName": null, - "props": null, + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"executeLog\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}", "mark": null, "attr1": null, "attr2": null, @@ -9885,10 +10095,10 @@ "defName": "日志信息", "intro": null, "orderValue": null, - "baseDataType": "VARCHAR", + "baseDataType": "TEXT", "bizDomainType": "", - "dbDataType": "VARCHAR", - "dataLen": null, + "dbDataType": "TEXT", + "dataLen": "", "numScale": null, "primaryKey": null, "notNull": 1, @@ -9931,9 +10141,9 @@ "defName": "错误信息", "intro": null, "orderValue": null, - "baseDataType": "VARCHAR", + "baseDataType": "TEXT", "bizDomainType": "", - "dbDataType": "VARCHAR", + "dbDataType": "TEXT", "dataLen": null, "numScale": null, "primaryKey": null, @@ -10045,7 +10255,7 @@ "defName": "任务调度记录表", "intro": null, "schemaName": null, - "props": null, + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"scheduleRecode\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}}}}", "mark": null, "attr1": null, "attr2": null, @@ -10802,7 +11012,7 @@ "defName": "定时任务表", "intro": null, "schemaName": null, - "props": null, + "props": "{\"codegen\":{\"NodeJS_Ts\":{\"root\":\"D:\\\\DaiMa\\\\WebStorm\\\\njzscloud-dispose-web\\\\src\\\\pages\",\"nameSpace\":\"sys\",\"baseName\":\"task\",\"path\":{\"page.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}.vue\",\"enable\":1},\"page.ts\":{\"enable\":1,\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/page.ts\"},\"form.vue\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.func.camel(it.genCode.baseName,true)}}Form.vue\",\"enable\":1},\"api.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}-api.ts\",\"enable\":1},\"d.ts\":{\"value\":\"{{=it.genCode.nameSpace}}/{{=it.genCode.baseName}}/{{=it.genCode.baseName}}.d.ts\",\"enable\":1}},\"var\":{}},\"Java_MybatisPlus\":{\"root\":\"D:\\\\DaiMa\\\\Idea\\\\njzscloud-dispose\\\\njzscloud-svr\\\\src\\\\main\\\\java\",\"nameSpace\":\"com.njzscloud.supervisory.sys\",\"baseName\":\"task\",\"path\":{\"Entity\":{\"enable\":1},\"Mapper\":{\"enable\":1},\"Mapper.xml\":{\"enable\":1},\"Service\":{\"enable\":1},\"Controller\":{\"enable\":1}},\"var\":{}}}}", "mark": null, "attr1": null, "attr2": null, @@ -10977,7 +11187,7 @@ "primaryKey": null, "notNull": 1, "autoIncrement": null, - "defaultValue": "INFO", + "defaultValue": "'INFO'", "stndDictId": null, "stndDictKey": null, "stndFieldId": null, @@ -11231,7 +11441,7 @@ "primaryKey": null, "notNull": 1, "autoIncrement": null, - "defaultValue": "", + "defaultValue": "''", "stndDictId": null, "stndDictKey": null, "stndFieldId": null, @@ -11263,99 +11473,6 @@ "updatedUserId": null, "createdUserId": null }, - { - "id": "ECEBD4EB-E3B7-4F2A-B6F7-E14F4D5D39B7", - "defKey": "creator_id", - "defName": "创建人 Id", - "intro": "sys_user.id", - "orderValue": null, - "baseDataType": "BIGINT", - "bizDomainType": "", - "dbDataType": "BIGINT", - "dataLen": null, - "numScale": null, - "primaryKey": null, - "notNull": 1, - "autoIncrement": null, - "defaultValue": null, - "stndDictId": null, - "stndDictKey": null, - "stndFieldId": null, - "stndFieldKey": null, - "mark": null, - "attr1": null, - "attr2": null, - "attr3": null, - "attr4": null, - "attr5": null, - "attr6": null, - "attr7": null, - "attr8": null, - "attr9": null, - "attr10": null, - "attr11": null, - "attr12": null, - "attr13": null, - "attr14": null, - "attr15": null, - "attr16": null, - "attr17": null, - "attr18": null, - "attr19": null, - "attr20": null, - "origin": null, - "stndComplianceType": null, - "stndComplianceLevel": null, - "updatedUserId": null, - "createdUserId": null - }, - { - "id": "982B3C15-A487-44B5-8FC1-922DF1A9BF48", - "defKey": "modifier_id", - "defName": "修改人 Id", - "intro": "sys_user.id", - "orderValue": null, - "baseDataType": "BIGINT", - "bizDomainType": "", - "dbDataType": "BIGINT", - "dataLen": null, - "numScale": null, - "primaryKey": null, - "notNull": 1, - "autoIncrement": null, - "defaultValue": null, - "stndDictId": "", - "stndDictKey": "", - "stndFieldId": null, - "stndFieldKey": null, - "mark": null, - "attr1": null, - "attr2": null, - "attr3": null, - "attr4": null, - "attr5": null, - "attr6": null, - "attr7": null, - "attr8": null, - "attr9": null, - "attr10": null, - "attr11": null, - "attr12": null, - "attr13": null, - "attr14": null, - "attr15": null, - "attr16": null, - "attr17": null, - "attr18": null, - "attr19": null, - "attr20": null, - "origin": null, - "stndComplianceType": null, - "stndComplianceLevel": null, - "updatedUserId": null, - "createdUserId": null, - "dictFrom": "" - }, { "id": "CF21B015-4E58-403E-9E88-5CFC1B6E2940", "defKey": "create_time", @@ -11401,98 +11518,6 @@ "stndComplianceLevel": null, "updatedUserId": null, "createdUserId": null - }, - { - "id": "DA97803E-70DD-4CD9-B237-880651056E28", - "defKey": "modify_time", - "defName": "修改时间", - "intro": "", - "orderValue": null, - "baseDataType": "DATETIME", - "bizDomainType": "", - "dbDataType": "DATETIME", - "dataLen": null, - "numScale": null, - "primaryKey": null, - "notNull": 1, - "autoIncrement": null, - "defaultValue": null, - "stndDictId": null, - "stndDictKey": null, - "stndFieldId": null, - "stndFieldKey": null, - "mark": null, - "attr1": null, - "attr2": null, - "attr3": null, - "attr4": null, - "attr5": null, - "attr6": null, - "attr7": null, - "attr8": null, - "attr9": null, - "attr10": null, - "attr11": null, - "attr12": null, - "attr13": null, - "attr14": null, - "attr15": null, - "attr16": null, - "attr17": null, - "attr18": null, - "attr19": null, - "attr20": null, - "origin": null, - "stndComplianceType": null, - "stndComplianceLevel": null, - "updatedUserId": null, - "createdUserId": null - }, - { - "id": "543065B6-52AB-4267-B385-BE1F9E61F8B8", - "defKey": "deleted", - "defName": "是否删除", - "intro": "0-->未删除、1-->已删除", - "orderValue": null, - "baseDataType": "TINYINT", - "bizDomainType": "", - "dbDataType": "TINYINT", - "dataLen": 1, - "numScale": null, - "primaryKey": null, - "notNull": 1, - "autoIncrement": null, - "defaultValue": "0", - "stndDictId": null, - "stndDictKey": null, - "stndFieldId": null, - "stndFieldKey": null, - "mark": null, - "attr1": null, - "attr2": null, - "attr3": null, - "attr4": null, - "attr5": null, - "attr6": null, - "attr7": null, - "attr8": null, - "attr9": null, - "attr10": null, - "attr11": null, - "attr12": null, - "attr13": null, - "attr14": null, - "attr15": null, - "attr16": null, - "attr17": null, - "attr18": null, - "attr19": null, - "attr20": null, - "origin": null, - "stndComplianceType": null, - "stndComplianceLevel": null, - "updatedUserId": null, - "createdUserId": null } ], "correlations": null, @@ -12734,67 +12759,6 @@ "attr20": "", "origin": "UI" }, - { - "id": "380D1990-AEEB-49A0-9144-99D5A764D8EB", - "defKey": "storage_type", - "defName": "库存类型", - "intro": "字典代码:storage_type", - "baseDataType": "VARCHAR", - "bizDomainType": "", - "dbDataType": "VARCHAR", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "storage_type", - "stndFieldId": "", - "stndDictKey": "storage_type", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "Manual", - "dictItems": [ - { - "itemKey": "Chu", - "itemName": "出库", - "parentKey": "", - "intro": "出库", - "id": "BF53A72D-73FE-4FE4-A3FA-8F23C5258B35" - }, - { - "itemKey": "Ru", - "itemName": "入库", - "parentKey": "", - "intro": "入库", - "id": "F0EBCDCC-0B35-4164-80BC-5567F3661E86" - } - ], - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, { "id": "EBF87EB9-8CD7-454E-8524-8AD01F924E4C", "defKey": "biz_type", @@ -12803,7 +12767,7 @@ "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": "", + "dataLen": 16, "numScale": "", "primaryKey": 0, "notNull": 1, @@ -13152,7 +13116,7 @@ "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": "", + "dataLen": 128, "numScale": "", "primaryKey": 0, "notNull": 1, @@ -13190,6 +13154,52 @@ "attr20": "", "origin": "UI" }, + { + "id": "67A16293-6A68-407C-A045-CB085BD0A181", + "defKey": "spec_params", + "defName": "规格", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, { "id": "DB75E8FF-191C-4B6E-A8DA-124AEC238F12", "defKey": "goods_name", @@ -13198,7 +13208,7 @@ "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": "", + "dataLen": 128, "numScale": "", "primaryKey": 0, "notNull": 1, @@ -13239,7 +13249,7 @@ { "id": "20E8584E-F1DE-4620-8B6F-DE6EA3DB4AF0", "defKey": "goods_category_id", - "defName": "产品类型Id", + "defName": "产品类型 Id", "intro": "", "baseDataType": "BIGINT", "bizDomainType": "", @@ -13328,52 +13338,6 @@ "attr20": "", "origin": "PASTE" }, - { - "id": "FCEC7D67-6297-42A7-9D17-5C6132F175BD", - "defKey": "cost_config_id", - "defName": "计费配置", - "intro": "", - "baseDataType": "BIGINT", - "bizDomainType": "", - "dbDataType": "BIGINT", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, { "id": "69C36FA2-0F56-421C-9FF0-051FC563FE64", "defName": "排序", @@ -13417,6 +13381,99 @@ "attr18": "PDManer", "attr19": "", "attr20": "", + "origin": "UI", + "defKey": "sort" + }, + { + "id": "A6AF8486-5774-49B9-B501-F97503BE0842", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "88E165E5-FC8F-4E07-87FD-A0E68628DFC4", + "defKey": "canuse", + "defName": "是否可用", + "intro": "0-->否、1-->是", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "1", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", "origin": "UI" }, { @@ -13469,7 +13526,7 @@ "id": "8AF726FB-0918-4FED-A0D0-604C48B01ADD", "defKey": "creator_id", "defName": "创建人 Id", - "intro": " sys_user.id", + "intro": "sys_user.id", "baseDataType": "BIGINT", "bizDomainType": "", "dbDataType": "BIGINT", @@ -13699,552 +13756,497 @@ "indexes": [] }, { - "id": "1CD9AEA6-DAB8-40DF-8318-BCA343176C13", - "schemaName": null, - "defKey": "cost_config", - "defName": "cost_config", - "intro": "", + "id": "FEF038F4-4AEE-4930-9C4B-E08E7F4F3A23", "type": "P", + "defKey": "sys_token", + "defName": "登录令牌表", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, "fields": [ { - "id": "7871BAC5-9C1E-45D3-A150-5D4C9B2D9772", + "id": "C19174D2-7943-4BDF-8316-432C36B91DDF", "defKey": "id", "defName": "Id", - "intro": "", + "intro": null, + "orderValue": null, "baseDataType": "BIGINT", "bizDomainType": "", "dbDataType": "BIGINT", - "dataLen": "", - "numScale": "", + "dataLen": null, + "numScale": null, "primaryKey": 1, "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": [], - "fieldTier": "", + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "PDManer", - "attr19": "", - "attr20": "", - "origin": "PASTE" + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null }, { - "id": "4FDB3C56-9FFA-4A13-AE1A-E764AE66431C", - "defKey": "cost_strategy", - "defName": "计费策略", - "intro": "字典代码:cost_strategy", + "id": "3618FFCA-CE3D-4A17-85F1-2CC0EA27D21C", + "defKey": "user_id", + "defName": "用户 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "0C71BB99-2CE3-49BE-9AAA-12237E6432BE", + "defKey": "tid", + "defName": "Token Id", + "intro": null, + "orderValue": null, "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", - "dataLen": 64, - "numScale": "", - "primaryKey": 0, + "dataLen": 128, + "numScale": null, + "primaryKey": null, "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "cost_strategy", - "stndFieldId": "", - "stndDictKey": "cost_strategy", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "Manual", - "dictItems": [ - { - "itemKey": "Dun", - "itemName": "按吨", - "parentKey": "", - "intro": "", - "id": "17C1DC4B-B707-4DAD-8B24-484838F9C157" - }, - { - "itemKey": "Che", - "itemName": "按方", - "parentKey": "", - "intro": "", - "id": "CC0058F6-02CA-4645-9FEF-9143833C2964" - }, - { - "itemKey": "Fang", - "itemName": "按距离", - "parentKey": "", - "intro": "", - "id": "024EEBD1-5C0E-4AA4-A5EA-3E2BAA80628A" - }, - { - "itemKey": "Juli", - "itemName": "按车", - "parentKey": "", - "intro": "", - "id": "34D2FFAC-CB4B-4E3B-8125-6605491E95A8" - }, - { - "itemKey": "TanXing", - "itemName": "弹性", - "parentKey": "", - "intro": "", - "id": "B247787C-6787-41E8-BADD-5F155C62B7A9" - } - ], - "fieldTier": "", + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "PASTE" + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null }, { - "id": "AA6A0858-0789-4ABA-B055-CD3F39C1A4D6", - "defKey": "tax_rate", - "defName": "税率", - "intro": "", - "baseDataType": "DECIMAL", - "bizDomainType": "", - "dbDataType": "DECIMAL", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "57999358-5DCB-400E-8A77-8F3A62665A9F", - "defKey": "payer", - "defName": "付费方", - "intro": "字典代码:payer", - "baseDataType": "VARCHAR", - "bizDomainType": "", - "dbDataType": "VARCHAR", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "payer", - "stndFieldId": "", - "stndDictKey": "payer", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "Manual", - "dictItems": [ - { - "itemKey": "PingTai", - "itemName": "平台", - "parentKey": "", - "intro": "", - "id": "F9E5B2B3-3E58-4B22-88F6-C3925BDD4EFF" - }, - { - "itemKey": "ChanFei", - "itemName": "产废方", - "parentKey": "", - "intro": "", - "id": "DAE6A790-C2D0-4972-9491-FEAF67E5B00C" - }, - { - "itemKey": "QingYun", - "itemName": "清运方", - "parentKey": "", - "intro": "", - "id": "BDD02160-F9E7-4E0B-A227-008E0E0DCBB9" - }, - { - "itemKey": "XiaoNa", - "itemName": "消纳方", - "parentKey": "", - "intro": "", - "id": "19C7D008-74BD-4478-8705-D5EB03C54DA5" - } - ], - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "5A28A88A-E522-47C6-A63E-01FF12177651", - "defKey": "canuse", - "defName": "是否可用", - "intro": "0-->否、1-->是", - "baseDataType": "TINYINT", - "bizDomainType": "", - "dbDataType": "TINYINT", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "301FA013-39AE-44C3-BF16-5C0A0B9E8355", - "defKey": "unit", - "defName": "单位", - "intro": "字典代码:unit", - "baseDataType": "VARCHAR", - "bizDomainType": "", - "dbDataType": "VARCHAR", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "38341EB7-4DE1-4AAC-955D-B0B3F08E54CC", - "defKey": "unit_price", - "defName": "单价", - "intro": "单位:元,弹性模式-->每档价格", - "baseDataType": "DECIMAL", - "bizDomainType": "", - "dbDataType": "DECIMAL", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "0.0", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "2356F174-F3CE-4F53-9243-DC25BF41609D", - "defKey": "initial_price", - "defName": "起步价", - "intro": "单位:元,<= 起步量 固定费用", - "baseDataType": "DECIMAL", - "bizDomainType": "", - "dbDataType": "DECIMAL", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "0.0", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "4F1B1CE5-54FF-4161-A825-95A6DF9DF02E", - "defKey": "initial_quantity", - "defName": "起步量", - "intro": "", - "baseDataType": "INT", - "bizDomainType": "", - "dbDataType": "INT", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "0", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "A0020FCD-FCA7-42E8-AAEB-871E1878A482", - "defKey": "every_quantity", - "defName": "每档的量", - "intro": "", - "baseDataType": "INT", - "bizDomainType": "", - "dbDataType": "INT", - "dataLen": "", - "numScale": "", - "primaryKey": 0, - "notNull": 1, - "autoIncrement": 0, - "defaultValue": "0", - "stndDictId": "", - "stndFieldId": "", - "stndDictKey": "", - "stndFieldKey": "", - "stndComplianceLevel": "", - "stndComplianceType": "", - "dictFrom": "", - "dictItems": null, - "fieldTier": "", - "mark": null, - "attr1": "", - "attr2": "", - "attr3": "", - "attr4": "", - "attr5": "", - "attr6": "", - "attr7": "", - "attr8": "", - "attr9": "", - "attr10": "", - "attr11": "", - "attr12": "", - "attr13": "", - "attr14": "", - "attr15": "", - "attr16": "", - "attr17": "", - "attr18": "", - "attr19": "", - "attr20": "", - "origin": "UI" - }, - { - "id": "41DD32FB-F933-4C6E-B990-E543EBDE9D63", - "defKey": "memo", - "defName": "备注", - "intro": "", + "id": "F6C2D625-5FC5-41DC-AFAD-5BE4E5BEA5DA", + "defKey": "tkey", + "defName": "Token 键", + "intro": null, + "orderValue": null, "baseDataType": "VARCHAR", "bizDomainType": "", "dbDataType": "VARCHAR", "dataLen": 512, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "C86DD7B3-38C5-4D83-B186-72384925B943", + "defKey": "tval", + "defName": "Token 值", + "intro": null, + "orderValue": null, + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "3DD1ECA7-0611-40F6-84AE-27A79450D5AA", + "defKey": "user_detail", + "defName": "用户信息", + "intro": null, + "orderValue": null, + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + } + ], + "correlations": null, + "indexes": [] + }, + { + "id": "9645DE46-7F49-4C24-99F2-9C8D88C5B328", + "type": "P", + "defKey": "wh_warehouse", + "defName": "仓库信息", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "E3CCB761-CD13-4844-9312-A73789EF5723", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": 1, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "E7104059-7973-4FB3-AA0C-25D03F55EAA2", + "defKey": "warehouse_name", + "defName": "仓库名称", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "AB843F40-8EA2-466B-9C7B-CF8D350AF1E1", + "defKey": "total_quantity", + "defName": "总量", + "intro": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", "numScale": "", "primaryKey": 0, "notNull": 1, "autoIncrement": 0, - "defaultValue": "''", + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "5F814C7B-2B16-432F-9EFF-E308C4106CE3", + "defKey": "used_quantity", + "defName": "已用", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", "stndDictId": "", "stndFieldId": "", "stndDictKey": "", @@ -14252,7 +14254,7 @@ "stndComplianceLevel": "", "stndComplianceType": "", "dictFrom": "", - "dictItems": [], + "dictItems": null, "fieldTier": "", "mark": null, "attr1": "", @@ -14272,13 +14274,151 @@ "attr15": "", "attr16": "", "attr17": "", - "attr18": "PDManer", + "attr18": "", "attr19": "", "attr20": "", "origin": "UI" }, { - "id": "99DEE8B8-55C9-4F64-9423-12F9AFB761CD", + "id": "2E84CC1D-B5A8-490D-9E4F-E44221AFFF41", + "defKey": "remaining_quantity", + "defName": "剩余量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "6FA8EBBB-DCA3-49A8-99B8-E3ACC9A22312", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "94991B9E-1303-406C-BD42-EDBDBE2454C9", + "defKey": "location", + "defName": "位置", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "A1989973-59EA-4336-B75D-4A09B84600A5", "defKey": "creator_id", "defName": "创建人 Id", "intro": " sys_user.id", @@ -14324,10 +14464,10 @@ "origin": "PASTE" }, { - "id": "09D256B0-1AA9-41C2-8EA1-90739E21AD2C", + "id": "F260333D-1C1F-4FB5-A6A8-6A724FE3CAB3", "defKey": "modifier_id", "defName": "修改人 Id", - "intro": " sys_user.id", + "intro": "sys_user.id", "baseDataType": "BIGINT", "bizDomainType": "", "dbDataType": "BIGINT", @@ -14370,7 +14510,7 @@ "origin": "PASTE" }, { - "id": "93952EEE-BDD1-4C8D-B795-D24A1E75B7C9", + "id": "458B9A92-69D1-4CDB-81B4-6739C8D53C33", "defKey": "create_time", "defName": "创建时间", "intro": "", @@ -14416,7 +14556,7 @@ "origin": "PASTE" }, { - "id": "251BA67A-9180-4ECB-BFD2-ACB590CFB138", + "id": "B1D24785-0CC4-4A13-A33A-9F2C5FFA3F13", "defKey": "modify_time", "defName": "修改时间", "intro": "", @@ -14462,7 +14602,6242 @@ "origin": "PASTE" }, { - "id": "E238E302-DAB7-4D0C-A5F8-ED959105CA58", + "id": "575DB731-B801-424D-AD9D-FFAB93F11AC3", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "EFC0D5C0-57FD-4BB8-9B16-153C3E34769E", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "04B05244-7665-40E6-9A98-BDB20746A886", + "type": "P", + "defKey": "wh_io_order", + "defName": "出入库单", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "6603B62B-FE85-484F-8EF5-C81B11AF3587", + "defKey": "id", + "defName": "主键", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "DE69F46B-C278-4A16-9CA5-F1D37FED3D55", + "defKey": "sn", + "defName": "编号", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D91A56A4-BD7F-44D2-A93B-6380A9D1A684", + "defKey": "io_type", + "defName": "出入库类型", + "intro": "", + "baseDataType": "CHAR", + "bizDomainType": "", + "dbDataType": "CHAR", + "dataLen": 3, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "io_type", + "stndFieldId": "", + "stndDictKey": "io_type", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Chu", + "itemName": "出库", + "parentKey": "", + "intro": "", + "id": "FEA2756B-70FE-4070-B0EB-DD20EA859F64" + }, + { + "itemKey": "Ru", + "itemName": "入库", + "parentKey": "", + "intro": "", + "id": "561AD9C9-17D5-4A1A-A653-A145400CA866" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "8A4066F4-9503-46B9-9656-CC88BCC04D7B", + "defKey": "order_category", + "defName": "订单类型", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": "order_category", + "stndDictKey": "order_category", + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null, + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "XiaoShou", + "itemName": "销售", + "parentKey": "", + "intro": "", + "id": "A7B0FB0C-CDB4-4AC3-BC1E-B178A82AA5B4" + }, + { + "itemKey": "CaiGou", + "itemName": "采购", + "parentKey": "", + "intro": "", + "id": "7938B08C-655D-46ED-B0C0-9F2BC79D66DD" + } + ] + }, + { + "id": "04A06DAD-E2A3-4238-912C-A6BB87BEF97B", + "defKey": "order_id", + "defName": "业务订单 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": 1, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "7723E97E-7F1F-432D-A7DA-4706BB0F16CD", + "defKey": "order_sn", + "defName": "单号", + "intro": "", + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "5D2CA9C5-0D85-452C-80D8-03056008D712", + "defKey": "warehouse_id", + "defName": "仓库 Id", + "intro": "wh_warehouse.id", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "385889C0-E756-48F3-8BA6-E12282F2AB12", + "defKey": "location", + "defName": "存放位置", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "''", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "30B361E4-69DC-47B8-A054-993334D1F1D9", + "defKey": "goods_category_id", + "defName": "产品分类 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "06A0B175-DCEC-407C-B50A-90639AF948C4", + "defKey": "goods_category_name", + "defName": "分类名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "044C1910-CB37-4BF5-B1B1-096C5CCBA596", + "defKey": "goods_id", + "defName": "产品 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "138A2CB9-CA3B-40DE-9E83-CDACE457102A", + "defKey": "goods_name", + "defName": "产品名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "F1266F3B-0E9A-4D20-8E10-9D3A862582EE", + "defKey": "good_sn", + "defName": "商品编码", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "198AC965-43F6-4DCA-B1F9-8D613FA2C1C9", + "defKey": "quantity", + "defName": "数量", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "60E619A9-F46E-4FA2-95C8-C5D008309685", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "73E9B263-6FC2-4CAF-9E5D-40E78CBBF3D2", + "defKey": "io_time", + "defName": "出入库时间", + "intro": null, + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "F42DE23F-C81C-4A98-8C16-E591F542559D", + "defKey": "responsible_id", + "defName": "责任人 Id", + "intro": "sys_user.id", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D77EEF3E-60D0-449F-8E94-619785DD8F1C", + "defKey": "memo", + "defName": "备注", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "1445C463-8DF1-46FD-91E6-83FE4EE2A228", + "origin": "UI" + }, + { + "id": "EF7583D8-A19D-4FB5-8052-80A886C707D4", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3E76AB4F-4F37-4CB0-8C03-942BD5A08718", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "45BB4EE6-53AA-4EB6-88C6-8D6F56F86DD2", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "26C9C1D8-B921-49B4-8E2C-582B3988DAE8", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3A8C7853-53D7-4C83-80B2-61F10D7F7251", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "B41E8EBF-1F5F-4DB9-BE5C-224CBAA5AD02", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "153541F6-C971-4324-B97C-BEF689627B24", + "type": "P", + "defKey": "wh_warehouse_check_plan", + "defName": "库存盘点计划", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "77F8F702-A08D-4C91-8859-2EA8A16FABAF", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": 1, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "47D7EC42-0B7E-4224-AAB8-C26A39AC842B", + "defKey": "sn", + "defName": "编号", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": "", + "stndDictKey": "", + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null, + "dictFrom": "" + }, + { + "id": "C831DF36-FB66-4CF3-851B-D4E81463C114", + "defKey": "plan_status", + "defName": "状态", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "plan_status", + "stndFieldId": "", + "stndDictKey": "plan_status", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "DaiZhiXing", + "itemName": "待执行", + "parentKey": "", + "intro": "", + "id": "781E9A57-8FB7-4D44-9CA0-1DB836BCC2FD" + }, + { + "itemKey": "ZhiXingZhong", + "itemName": "执行中", + "parentKey": "", + "intro": "", + "id": "DB8E76CA-21CE-4B1E-95D4-A57534E40E92" + }, + { + "itemKey": "WanCheng", + "itemName": "完成", + "parentKey": "", + "intro": "", + "id": "C68C27DC-9927-4F55-B71A-8DED55F6743C" + }, + { + "itemKey": "QuXiao", + "itemName": "取消", + "parentKey": "", + "intro": "", + "id": "C8D688B5-3D4D-477F-A453-C4CD8D78F470" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "DA766A3D-1668-42C6-9CFB-1F9D00103263", + "defKey": "warehouse_id", + "defName": "盘点仓库 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "2AEF93A4-2704-41E4-BDC6-768DBC192EE9", + "defKey": "plan_complete_time", + "defName": "计划完成时间", + "intro": null, + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D1DE9F74-74BF-441B-9040-AC9363F9FDB9", + "defKey": "complete_time", + "defName": "实际完成时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "33580E5A-0E39-469F-A0DC-A18904B89557", + "defKey": "memo", + "defName": "备注", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "''", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": "PDManer", + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null, + "dictItems": [] + }, + { + "id": "D7804FC1-95F1-450A-8DB0-DA3FC330ACF8", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "A98E8CEC-565C-47EF-880D-0275DFB81451", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "0974A666-5A74-48A9-81FE-5B87567103B7", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "CB978321-039B-488F-8CF7-62D7BCE3DD3F", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "8126165A-6867-4137-AB42-F7B9BB0390AD", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "F78B55BE-2EBD-42BE-9171-C5812DDD7355", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "C9A36E0B-ECA6-4D3C-AEE5-691F607359F6", + "type": "P", + "defKey": "wh_warehouse_check_detail", + "defName": "库存盘点明细", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "7BBA30CF-D4AC-4F9D-893F-58B6B25513A8", + "defKey": "id", + "defName": "主键", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "E39ADA63-4F09-4672-B64A-03C02BAD2101", + "defKey": "check_plan_id", + "defName": "盘点计划 Id", + "intro": "wh_warehouse_check_plan.id", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "EE7E293A-6EC1-4D04-A76E-F52737A07CB6", + "defKey": "goods_category_id", + "defName": "产品分类 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "4BC7A3ED-175D-4669-9F3C-36E61A089679", + "defKey": "goods_category_name", + "defName": "分类名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "2F701052-F9DB-49BA-8176-E182F489B4FD", + "defKey": "goods_id", + "defName": "产品 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "17F4EF19-C77C-4752-A8D7-E87FCAAE3645", + "defKey": "goods_name", + "defName": "产品名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "5580F963-B199-4ACC-9D26-AD0C136498F7", + "defKey": "good_sn", + "defName": "商品编码", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3029D80F-1F00-456B-B7A8-ACD0A8F80903", + "defKey": "system_quantity", + "defName": "系统库存", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "7E1A0F9B-58A9-4023-87A3-DDDEC0AFC0DB", + "defKey": "actual_quantity", + "defName": "实际库存", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "430DE5FA-856A-4E2C-895C-D7612E91289E", + "defKey": "difference_quantity", + "defName": "差异数量", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "3C79D303-EE0D-439E-A280-78F7EFDA36E7", + "defKey": "suggestion", + "defName": "处理建议", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "''", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D4A4CC80-6768-43FB-93F7-9FCB99A04FDD", + "defKey": "check_time", + "defName": "盘点时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "CD701A1D-4DFD-4C3D-852C-45A0D703868C", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "7AB6AB06-3DAC-44EB-A8C2-1C7A9EEE57D1", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "066F13FF-39D3-4A3C-9589-07C2B0DA02B7", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "B8AEA916-E963-4398-BFB7-9DE36AD47918", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FED8B3D2-13D3-4E45-8E55-E22C2FAB03CD", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "FA0EF49D-FCB7-468E-AB9C-3AF02C54EBB7", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "3B83C23F-D49C-4D67-830C-882D91A57927", + "type": "P", + "defKey": "fin_money_account", + "defName": "资金账户表", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "085FB24C-8DFE-4CE7-A346-BC10D51F769E", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "93ECE81F-6496-4363-A00E-77AAD809AE16", + "defKey": "sn", + "defName": "账户编号", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "726C3A08-55D3-42DE-9502-134B8AFEB16D", + "defKey": "account_type", + "defName": "账户类型", + "intro": "account_type", + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": "account_type", + "stndDictKey": "account_type", + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null, + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "GeRen", + "itemName": "个人", + "parentKey": "", + "intro": "", + "id": "7E8BA416-A591-4600-8C9F-D94798599FF2" + }, + { + "itemKey": "QiYe", + "itemName": "企业", + "parentKey": "", + "intro": "", + "id": "A36987CD-B68D-4C8F-8098-60982342EDE3" + } + ] + }, + { + "id": "437EDEA8-4826-495B-B554-77DBF9216FE5", + "defKey": "user_id", + "defName": "用户 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "9A445C08-3EA4-48A5-8204-E98C0A919F8E", + "defKey": "company_id", + "defName": "企业 Id", + "intro": "企业用户有值", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": null, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "4BECBB17-AC7A-42A2-8DCF-687E42EE4984", + "defKey": "revenue", + "defName": "营收余额", + "intro": null, + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0.0", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "3FE01194-6BCF-4B41-8CA5-A3DB08FA8792", + "defKey": "recharge", + "defName": "充值余额", + "intro": "", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0.0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "F9A14E61-2481-4CCB-B979-D524F0B06FAF", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "786CBCD8-E973-4BCF-AD94-2C58E11305EF", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "05FA1902-051B-4BE8-821B-C6F50AFF95AD", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "F1D3228D-2BA3-484F-885F-2F22D97C8431", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "0CC26181-8E29-48AA-B96E-6375467C7C36", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "E4AF94E6-6AF3-45CF-82EF-BF865FC480E5", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "EDDB991D-570B-493F-AF67-97787AC9BAFC", + "type": "P", + "defKey": "fin_money_detail", + "defName": "资金明细", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "314F2AC3-D4C0-473A-A074-2328879D36AD", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "82FE1E45-8AB6-42BD-9E01-767F77F98648", + "defKey": "money_account_id", + "defName": "资金账户 Id", + "intro": "fin_money_account.id", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "E61D92DE-71AB-4AD8-8BAE-A4CD9EDCB917", + "defKey": "bofore_balance", + "defName": "变动前余额", + "intro": null, + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "CA079A95-43C9-4067-8AF4-802839BB8AA5", + "defKey": "delta", + "defName": "变动金额", + "intro": "有正负", + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "B0557BF0-0718-47FC-82C7-CAAFD64AB6A2", + "defKey": "after_balance", + "defName": "变动后余额", + "intro": null, + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "9A0182DF-EF32-4427-A61E-F7871352CE18", + "defKey": "money_change_category", + "defName": "变动类型", + "intro": "字典代码:money_change_category", + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": "money_change_category", + "stndDictKey": "money_change_category", + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null, + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "ChongZhi", + "itemName": "充值", + "parentKey": "", + "intro": "", + "id": "3CECA6B1-D3AB-41D4-8436-2A78BA3FAE21" + }, + { + "itemKey": "YingShou", + "itemName": "营收", + "parentKey": "", + "intro": "", + "id": "558621C0-3219-41F8-8FC1-D40571D7BC8A" + }, + { + "itemKey": "TuiKuan", + "itemName": "退款", + "parentKey": "", + "intro": "", + "id": "3BCCE848-9201-45BD-832A-EB8C1F2C35F0" + }, + { + "itemKey": "TiaoZhang", + "itemName": "调账", + "parentKey": "", + "intro": "", + "id": "268A1731-DE1D-4E00-9FAF-51F3AECE8430" + }, + { + "itemKey": "ZhiFu", + "itemName": "支付", + "parentKey": "", + "intro": "", + "id": "84CD56DB-9083-4844-974D-000CF485FEC6" + } + ] + }, + { + "id": "C8407B1C-1AD4-401B-B617-AD520461E229", + "defKey": "order_id", + "defName": "订单 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": null, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "4C7365AD-B9B7-4F4F-8C14-FD3C14483097", + "defKey": "memo", + "defName": "备注", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "8F8561D4-DE39-4E27-B842-595BD8E078D3", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "19DB1E0C-0C78-4D7A-8E38-8C1698041193", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "97287E91-D09E-45A7-9F09-250FFC4A12A0", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "15B674F7-FC26-4383-B98C-0D5F5C06F45B", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FCDD9BEE-A8B4-4499-A6A8-AF65CF615440", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "0740FC97-B36D-4C37-B501-633273E2FCAD", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "48C64C11-5A89-4DDD-95A6-3EBF40F399FB", + "type": "P", + "defKey": "fin_bill", + "defName": "对账单", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "574443F6-C61B-4C10-ADA5-2A9149578C3F", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": 1, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "3D3621C8-5E84-44AA-B722-CB8653F94FDF", + "defKey": "user_id", + "defName": "用户 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "F8F12C09-54A4-47A4-9ED6-4AFA8F23382A", + "defKey": "company_id", + "defName": "企业 Id", + "intro": "企业用户有值", + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 0, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "7CDBFB14-C98C-4646-9B09-F137B98A75CE", + "defKey": "account_type", + "defName": "账户类型", + "intro": "account_type", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": "account_type", + "stndFieldId": null, + "stndDictKey": "account_type", + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "GeRen", + "itemName": "个人", + "parentKey": "", + "intro": "", + "id": "7E8BA416-A591-4600-8C9F-D94798599FF2" + }, + { + "itemKey": "QiYe", + "itemName": "企业", + "parentKey": "", + "intro": "", + "id": "A36987CD-B68D-4C8F-8098-60982342EDE3" + } + ], + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "0D3E1F50-E791-4A32-8533-70FA8B155D7A", + "defKey": "bill_period", + "defName": "账期", + "intro": null, + "orderValue": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "FDE4D297-CBEA-4EFB-B8FA-CD7983B13375", + "defKey": "start_time", + "defName": "账单开始时间", + "intro": null, + "orderValue": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "BBFC8F86-77BF-41AB-9ACC-052DDD17353B", + "defKey": "end_time", + "defName": "账单结束时间", + "intro": null, + "orderValue": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "421FEC0B-53AA-4E01-ADE6-59395524004F", + "defKey": "order_count", + "defName": "订单数", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "0B34B198-82A7-4E99-A190-351393FB31A2", + "defKey": "car_count", + "defName": "总车数", + "intro": null, + "orderValue": null, + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "6812CCA8-26E3-4C1A-A6DB-010C7B19C536", + "defKey": "total_weight", + "defName": "总质量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "6A771FD2-567C-4570-A211-B74CE314FFB5", + "defKey": "discount_amount", + "defName": "优惠金额", + "intro": null, + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "A4A35716-01F5-4C3F-8437-41023CEF6FE7", + "defKey": "total_amount", + "defName": "账单金额", + "intro": null, + "orderValue": null, + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 14, + "numScale": 2, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "F522374B-493B-48B7-BBA7-4725373AF427", + "defKey": "memo", + "defName": "备注", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "932046B0-9B90-4AA7-8A85-F1B83FF7415F", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "1959BF80-867D-4AF4-BC96-BE5805B7DEBF", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FF505CF8-9A8C-424A-8B7F-0BC8AEC4A6F6", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "BFC18E41-E63C-4D34-841C-0BE8C484F506", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "9DDF41D6-549B-4DE3-8C5B-A6E52C9B02D0", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [ + { + "id": "8EE3F795-1F2E-4291-9F52-CC81E769D2C5", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "4476CB18-4B15-486E-A0AD-DA49AB267305", + "type": "P", + "defKey": "fin_discount", + "defName": "优惠管理", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "7CD41FF3-69C7-4146-BA7F-8A32F2DA8EDF", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "0A558CBD-8FEC-4AF6-988C-EF90FF0C17A4", + "defKey": "expense_item_id", + "defName": "收费项 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "316F4C5B-98CE-4863-BA6B-E946D8D666D5", + "defKey": "company_id", + "defName": "公司 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "DF7D92B4-D59E-4D4C-B656-89BDFD896E37", + "defKey": "delta_amount", + "defName": "优惠金额", + "intro": null, + "orderValue": null, + "baseDataType": "DOUBLE", + "bizDomainType": "", + "dbDataType": "DOUBLE", + "dataLen": 16, + "numScale": 6, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0.00", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "A451408C-918D-4C20-9925-117E026093ED", + "defKey": "start_time", + "defName": "有效期", + "intro": "开始", + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "48BB9F97-50F4-4442-8DF5-B8A082E6D4B3", + "defKey": "end_time", + "defName": "有效期", + "intro": "结束", + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + } + ], + "correlations": null, + "indexes": [ + { + "id": "15FC8B19-A4F6-4955-B2D5-5C42A4E6BC56", + "type": "PRIMARY KEY", + "defKey": null, + "defName": null, + "intro": null, + "orderValue": null, + "fields": [] + } + ] + }, + { + "id": "31EE5227-5597-42B8-8D7A-B558F708C076", + "schemaName": null, + "defKey": "fin_expense_item", + "defName": "收费项目", + "intro": "", + "type": "P", + "fields": [ + { + "id": "306F745A-B99D-4639-94FD-A4AF36D63219", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "489F7BA3-FC15-4C50-8EE6-F0A529DD2349", + "defKey": "expense_item_category", + "defName": "收费项目类型", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "expense_item_category", + "stndFieldId": "", + "stndDictKey": "expense_item_category", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "QiTa", + "itemName": "其他", + "parentKey": "", + "intro": "", + "id": "B4560F4E-C17C-45E6-BA46-F24447710D73" + }, + { + "itemKey": "ChanPin", + "itemName": "产品", + "parentKey": "", + "intro": "", + "id": "1078D9EB-D7B7-4AE4-BB59-C5981DFFE609" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "69F9B61C-E6EE-4AD1-8071-4C3BE2918F44", + "defKey": "expense_item_name", + "defName": "付费项名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "190594CC-5FDA-4A8C-8586-6B1E099A9C96", + "defKey": "expense_strategy", + "defName": "计费策略", + "intro": "字典代码:expense_strategy", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 64, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "expense_strategy", + "stndFieldId": "", + "stndDictKey": "expense_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "MianFei", + "itemName": "免费", + "parentKey": "", + "intro": "", + "id": "7B74209A-E0F9-4D7E-B437-A15802FAC11B" + }, + { + "itemKey": "TanXing", + "itemName": "弹性", + "parentKey": "", + "intro": "", + "id": "B247787C-6787-41E8-BADD-5F155C62B7A9" + }, + { + "itemKey": "Juli", + "itemName": "按车", + "parentKey": "", + "intro": "", + "id": "34D2FFAC-CB4B-4E3B-8125-6605491E95A8" + }, + { + "itemKey": "Dun", + "itemName": "按吨", + "parentKey": "", + "intro": "", + "id": "17C1DC4B-B707-4DAD-8B24-484838F9C157" + }, + { + "itemKey": "Che", + "itemName": "按方", + "parentKey": "", + "intro": "", + "id": "CC0058F6-02CA-4645-9FEF-9143833C2964" + }, + { + "itemKey": "Fang", + "itemName": "按距离", + "parentKey": "", + "intro": "", + "id": "024EEBD1-5C0E-4AA4-A5EA-3E2BAA80628A" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "00DE49BD-389F-45AB-A62C-BAB44555C4E6", + "defKey": "tax_rate", + "defName": "税率", + "intro": "", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 10, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "2C053A34-F665-4AE5-9DD2-540EE3AF25B7", + "defKey": "payer", + "defName": "付费方", + "intro": "字典代码:payer", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "payer", + "stndFieldId": "", + "stndDictKey": "payer", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "PingTai", + "itemName": "平台", + "parentKey": "", + "intro": "", + "id": "F9E5B2B3-3E58-4B22-88F6-C3925BDD4EFF" + }, + { + "itemKey": "ChanFei", + "itemName": "产废方", + "parentKey": "", + "intro": "", + "id": "DAE6A790-C2D0-4972-9491-FEAF67E5B00C" + }, + { + "itemKey": "QingYun", + "itemName": "清运方", + "parentKey": "", + "intro": "", + "id": "BDD02160-F9E7-4E0B-A227-008E0E0DCBB9" + }, + { + "itemKey": "XiaoNa", + "itemName": "消纳方", + "parentKey": "", + "intro": "", + "id": "19C7D008-74BD-4478-8705-D5EB03C54DA5" + }, + { + "itemKey": "CaiGou", + "itemName": "采购方", + "parentKey": "", + "intro": "", + "id": "0EF79E9D-AF63-43B5-A7CB-645CA1C01597" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "201E1E68-611D-4AB9-A01D-3175C130DAA1", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "4EB5517F-F6B8-491D-B19B-BC831FB0EB03", + "defKey": "unit_price", + "defName": "单价", + "intro": "单位:元,弹性模式-->每档价格", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 16, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0.0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "E3F29B4E-00C2-407F-BD13-A0D1C8A91DB7", + "defKey": "initial_price", + "defName": "起步价", + "intro": "单位:元,<= 起步量 固定费用", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 16, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0.0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "75049E3E-2557-43FB-872C-98BF1FBFB4DD", + "defKey": "initial_quantity", + "defName": "起步量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "40ECC628-4D15-4DF9-83CB-6BA83F0081C6", + "defKey": "every_quantity", + "defName": "每档的量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "99BD14DF-F3A1-46A7-8238-3CC7B7AA3745", + "defKey": "user_scope", + "defName": "适用用户", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "2FCE3A67-11F3-434C-A3EB-D7792AFF623D", + "defKey": "station_scope", + "defName": "适用站点", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "DE960F53-340C-44C7-8108-67F725944E22", + "defKey": "goods_scope", + "defName": "适用产品", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "99457AD3-D5E3-41DA-B1A4-443A6433C97F", + "defKey": "canuse", + "defName": "是否可用", + "intro": "0-->否、1-->是", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "1", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "AA43F666-C944-4707-A6DB-ECDD1DD4969E", + "defKey": "memo", + "defName": "备注", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "CBF8F0DC-0EB3-4AB4-B11D-7DAC37691369", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "F9A513B5-2E81-43AB-AFE7-33E9672160B8", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "47D3340E-671F-46F3-AF66-569D27EED2D5", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "6C06ECA4-65AE-4564-9753-5EF8C606216F", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "EC809299-BE7E-437D-BDFA-D604D47200AD", "defKey": "deleted", "defName": "是否删除", "intro": " 0-->未删除、1-->已删除", @@ -14509,13 +20884,8033 @@ } ], "indexes": [] + }, + { + "id": "0B223F50-E8AF-40F4-8444-1B22D1171407", + "schemaName": null, + "defKey": "mfg_craft", + "defName": "工艺", + "intro": "", + "type": "P", + "fields": [ + { + "id": "363DC7F6-155A-4679-8E07-DCC249DB544C", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "759FF56C-6610-43D2-B4FA-7617DD2C65CB", + "defKey": "sn", + "defName": "编码", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "B02465CC-8EBC-4387-832D-C64B9ABC602F", + "defKey": "craft_name", + "defName": "工艺名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "B01F78EC-1B7F-4D60-8B1C-37A3B816107D", + "defKey": "major", + "defName": "主版本号", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "D05BCB6F-D34F-47B2-AE67-A14685E5752C", + "defKey": "minor", + "defName": "次版本号", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "46CC349F-6837-41D9-AC73-92852C444BAC", + "defKey": "patch", + "defName": "修订版本号", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "F02B36BC-9C06-40D7-A478-DC5789CCEFB3", + "defKey": "canuse", + "defName": "是否可用", + "intro": "0-->否、1-->是", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "1", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "532517BD-A9D0-484A-9EDA-93EC38A47E37", + "defKey": "memo", + "defName": "备注", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "2F962249-EDE4-4C9E-B48C-3710BD612C5F", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "51976034-6142-4362-81B1-469BABE7EA00", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "9E4C8E61-F843-4862-8BE7-215A395E2AD7", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "C41FDEAA-B07D-46F8-8328-81128EE2BE53", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "51F08199-B0D6-4C7C-A29B-1A845420260B", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "8597F6A3-0AB9-4885-BC3A-72C3ACC4B756", + "schemaName": null, + "defKey": "mfg_craft_detail", + "defName": "工艺明细", + "intro": "", + "type": "P", + "fields": [ + { + "id": "6483427C-D722-4E9F-91F3-FCBC420F83CA", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "21C0B59F-E430-48B3-BA69-44EB64563F5F", + "defKey": "goods_category_id", + "defName": "产品分类 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "A2ACB4FE-9159-410C-87A3-B651AB84813E", + "defKey": "goods_category_name", + "defName": "分类名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FD638B8D-7007-4FED-A0DB-7F8A5BB47AE4", + "defKey": "goods_id", + "defName": "产品 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "10DA759A-9CF4-471D-B7FA-7253187E5892", + "defKey": "goods_name", + "defName": "产品名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "BCB7084D-EEE9-4317-B898-6816A4540A09", + "defKey": "good_sn", + "defName": "商品编码", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "AA00368A-1AC4-454D-BF79-90ED9A27C648", + "defKey": "spec_params", + "defName": "规格", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "''", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "6209022C-2497-43AD-88CD-99AA4DBC8D95", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "A504D83B-2DDB-4E31-A93A-B1006254E832", + "defKey": "quantity", + "defName": "数量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "460AA422-A417-4EBB-A53E-5AFD414DC539", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "4CA887DE-1125-44CB-BBF4-BA13FA08921E", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "C1391B63-3D49-4099-8A6A-CE12226285D4", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "9AD8416E-AF46-4942-9FF0-D038A0C30050", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "6768B7A7-8D66-43CE-A8E8-120371B55680", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "1A2D551C-214D-469D-B0C5-917C29D725B2", + "schemaName": null, + "defKey": "cst_org", + "defName": "组织信息", + "intro": "", + "type": "P", + "fields": [ + { + "id": "CF671F2B-4BB0-4390-A384-D022A4F74FB3", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "C9A3D3BE-9969-4D88-82D0-DF5A71E45F3C", + "defKey": "entity_category", + "defName": "主体类型", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "entity_category", + "stndFieldId": "", + "stndDictKey": "entity_category", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "GeRen", + "itemName": "个人", + "parentKey": "", + "intro": "", + "id": "B692D2F8-14DC-48C3-B59C-AD3EDD447DAD" + }, + { + "itemKey": "GeTiHu", + "itemName": "个体户", + "parentKey": "", + "intro": "", + "id": "D82852C8-C638-47D2-AAF0-05FA88E172C9" + }, + { + "itemKey": "QiYe", + "itemName": "企业", + "parentKey": "", + "intro": "", + "id": "27282DC9-2018-4EF2-B5F8-9EC443950C68" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "8A05AFF6-C71E-4AB7-97C9-D1B47ACBD827", + "defKey": "identity_category", + "defName": "身份类型", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "identity_category", + "stndFieldId": "", + "stndDictKey": "identity_category", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "PingTai", + "itemName": "平台", + "parentKey": "", + "intro": "", + "id": "F9E5B2B3-3E58-4B22-88F6-C3925BDD4EFF" + }, + { + "itemKey": "ChanFei", + "itemName": "产废方", + "parentKey": "", + "intro": "", + "id": "DAE6A790-C2D0-4972-9491-FEAF67E5B00C" + }, + { + "itemKey": "QingYun", + "itemName": "清运方", + "parentKey": "", + "intro": "", + "id": "BDD02160-F9E7-4E0B-A227-008E0E0DCBB9" + }, + { + "itemKey": "XiaoNa", + "itemName": "消纳方", + "parentKey": "", + "intro": "", + "id": "19C7D008-74BD-4478-8705-D5EB03C54DA5" + }, + { + "itemKey": "CaiGou", + "itemName": "采购方", + "parentKey": "", + "intro": "", + "id": "0EF79E9D-AF63-43B5-A7CB-645CA1C01597" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "CCABEE4A-6B49-4AEC-9CF6-685292310D96", + "defKey": "uscc", + "defName": "统一社会信用代码", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "D5563299-8318-4937-A7DB-5C7FA8E25A3B", + "defKey": "org_name", + "defName": "公司名称", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "88AC98FE-11B5-45C0-8C87-6D50AFCD41F1", + "defKey": "business_license", + "defName": "营业执照", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "7E1F0EBE-DC1F-4C3A-BC77-D6EEAD93AAD3", + "defKey": "license_start_time", + "defName": "营业执照有效期", + "intro": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "1BEDF200-3ABA-4A4B-9A15-A7E499BEC6E6", + "defKey": "license_end_time", + "defName": "营业执照有效期", + "intro": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "EF682866-010D-4E75-B1F6-8EA15BB274A9", + "defKey": "legal_representative", + "defName": "法人名称", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "D1FADF9C-B9AB-4889-B1FD-A4177C5C5D8D", + "defKey": "idcard", + "defName": "法人身份证号", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 512, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "543C52A5-F915-4EDF-9EE7-DA05528AF26F", + "defKey": "idcard_start_time", + "defName": "法人身份证号有效期", + "intro": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "FA2B7D61-6038-48F9-AB54-62F27F2CD0E5", + "defKey": "idcard_end_time", + "defName": "法人身份证号有效期", + "intro": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "A8732082-E541-4C02-8471-EBFA4632B23E", + "defKey": "idcard_front", + "defName": "身份证正面", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "B69F7E6F-70D9-4DFA-91AE-79ED413B3429", + "defKey": "idcard_back", + "defName": "身份证反面", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "B625B190-7E83-48F2-801C-B09B0A73B108", + "defKey": "province", + "defName": "省", + "intro": "代码", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "F81C601B-BF3E-40E1-87A9-D6A8D7DB9D8F", + "defKey": "city", + "defName": "市", + "intro": "代码", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "D0EAF537-0C11-41B1-9582-C6613DA0BEF4", + "defKey": "area", + "defName": "区县", + "intro": "代码", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "75D6AB98-7A05-43DA-A688-193A24537838", + "defKey": "town", + "defName": "乡镇街道", + "intro": "代码", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "56CCC921-4E1A-43AF-81CA-8E66710E7423", + "defKey": "province_name", + "defName": "省", + "intro": "名称", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "3AECBDCD-F654-4658-9368-CFD1847DAD87", + "defKey": "city_name", + "defName": "市", + "intro": "名称", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "87B4AF6D-7726-491C-9EE1-D5137FFDA4D8", + "defKey": "area_name", + "defName": "区县", + "intro": "名称", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "BB53FA76-8C34-4F1C-AC8E-3EAFF0ED21B1", + "defKey": "town_name", + "defName": "乡镇街道", + "intro": "名称", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "B4FB38E5-DD24-4176-A757-A6A14B2C7EA5", + "defKey": "address", + "defName": "详细地址", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "70D04DAC-AE65-4C84-BDCF-4ED4465D484B", + "defKey": "lng", + "defName": "经度", + "intro": null, + "baseDataType": "DOUBLE", + "bizDomainType": "", + "dbDataType": "DOUBLE", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "1CC4F506-64C5-4882-B880-7553E3B91CA6", + "defKey": "lat", + "defName": "纬度", + "intro": null, + "baseDataType": "DOUBLE", + "bizDomainType": "", + "dbDataType": "DOUBLE", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "3FF6E15E-6352-4054-A8AC-DE2CFB613E93", + "defKey": "contacts", + "defName": "联系人", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "6C5AC943-1992-43A1-A542-88ACC7204D3C", + "defKey": "phone", + "defName": "联系电话", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "465B8DE0-CBA3-48AD-BE5C-74AC8414854C", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3838BCFD-CB87-4FF9-AAD9-BC722F6261D6", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "86E987F7-BCDE-47C9-BE16-55D5944BDCB0", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "B9CB3969-D9D3-4069-ABCB-DB181101B9A5", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "BF60EF09-20EA-41CF-AD26-AEC923D100D0", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "DECB0762-9BE3-429A-81F7-891B7C960B04", + "schemaName": null, + "defKey": "cst_customer", + "defName": "客户信息", + "intro": "", + "type": "P", + "fields": [ + { + "id": "09854DFF-61E6-4C00-ADC5-66F9B1708153", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "14C8CD6D-9945-48AC-B4DB-9876B85FA61D", + "defKey": "user_id", + "defName": "用户 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "6A72C42D-5DF7-425F-B444-FD4FB5B2CEA0", + "defKey": "org_id", + "defName": "主体信息", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "CD510B62-B163-4601-800C-4DB357BC1AAF", + "defKey": "org_manager", + "defName": "是否为管理员", + "intro": "", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "F029192F-84B3-4AF0-805D-612239BDC60B", + "defKey": "driver", + "defName": "是否为司机", + "intro": "", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "304F29BE-D238-4ABA-BB54-AAFF00126CC3", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "9D32B608-D5E3-4F6D-92CD-020CF45166E0", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "6DB5685B-E380-45AE-BFD6-991BEE95C127", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FCB7958D-5504-4223-9573-BE66E844B078", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "BDC5A81F-7A16-437C-A4ED-77544617A071", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "718C57B0-4799-48E1-8651-57527A3BFF8B", + "schemaName": null, + "defKey": "cst_truck", + "defName": "车辆信息", + "intro": "", + "type": "P", + "fields": [ + { + "id": "1EFCD46E-726C-4EA4-8405-FAAD1C783FB5", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "D3603202-48B0-46D6-87B6-BC4943E82C58", + "defKey": "org_id", + "defName": "归属企业", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "7E3DB6C8-9954-4E57-839D-BD514764EA3A", + "defKey": "license_plate", + "defName": "车牌", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "505C54D8-41F8-4A42-A898-8316979B4513", + "defKey": "truck_license", + "defName": "行驶证图片", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "A9B789BE-8B48-4C7A-B001-DE65D07B7ED7", + "defKey": "vn_code", + "defName": "车架号", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 64, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "BE19D8AF-C716-4B1D-ADF2-9593F34C4813", + "defKey": "qualification", + "defName": "合格证图片", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "EB79C2D2-4880-454C-930E-DAFE351C9A07", + "defKey": "carrying_capacity", + "defName": "最大载重", + "intro": "单位:千克", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "2E24E576-DA15-4A1A-9926-6056A17A5637", + "defKey": "tare_weight", + "defName": "皮重", + "intro": "单位:千克", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": null, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "7945761E-B26D-4DDC-BA19-70C4D98FE4C1", + "defKey": "license_start_date", + "defName": "行驶证有效期", + "intro": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": "", + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "020DEC28-1F6C-4E2B-9623-CEE38A3FB4EC", + "defKey": "license_end_date", + "defName": "行驶证有效期", + "intro": "", + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "B28A0666-5CF3-4D96-A876-D2BC07677BFA", + "defKey": "qualification_start_date", + "defName": "合格证有效期", + "intro": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": "", + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "CFE522E1-02C1-413D-A3B8-A01FF0256A73", + "defKey": "qualification_end_date", + "defName": "合格证有效期", + "intro": null, + "baseDataType": "DATE", + "bizDomainType": "", + "dbDataType": "DATE", + "dataLen": "", + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "BAD4F507-DCBD-4EE1-8685-3969FA4AC5D0", + "defKey": "truck_category", + "defName": "车辆类型", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 64, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "5C7F1E15-6F33-43FC-AF2C-10849008E06E", + "defKey": "picture", + "defName": "车辆图片", + "intro": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": null, + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "20A5707E-1063-45FE-BC7E-CC2E0F74FF12", + "defKey": "busy", + "defName": "忙碌中", + "intro": null, + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": null, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": null, + "stndFieldId": null, + "stndDictKey": null, + "stndFieldKey": null, + "stndComplianceLevel": null, + "stndComplianceType": null, + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": "PASTE" + }, + { + "id": "653C9BE5-4E40-4AA8-B336-458BD8223DB6", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "8B50C397-0ACA-4501-9E67-23E3ADB6577F", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "ABD071EE-06D0-4BF2-8F74-9F6CABF86DA2", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FD6086F7-EB89-4AA4-8501-1C829AE77CAC", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "BD7DBE88-568E-41D6-9A78-D635619C89C3", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "F13139EC-3554-4C1E-8C09-1A6C238C539A", + "schemaName": null, + "defKey": "cst_order", + "defName": "收/销订单", + "intro": "", + "type": "P", + "fields": [ + { + "id": "4B18692F-1CB5-4710-9058-946FF53D67F4", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "D21BF29B-542A-49F8-8F7F-F186A32AFA2B", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3D005849-86B8-482E-B2B7-4164FC366072", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "D9A7D167-1973-498F-AE69-5245744F2A46", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FA7D42D7-05A9-463E-BB7E-DD3F234F5D6B", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "282F8E9E-37AA-4E88-936F-0669D3BFD911", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "38B910DC-4D79-448E-9EA4-CED1E92B03DE", + "schemaName": null, + "defKey": "cst_order_trans", + "defName": "运输信息", + "intro": "", + "type": "P", + "fields": [ + { + "id": "BA82BEB0-62A6-410C-B352-68478E6656A6", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "C42086C9-14DA-4E59-8C95-3CE20BD542A1", + "defKey": "order_id", + "defName": "订单 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "EC5E439D-4BC3-41FF-91CC-586E68D8615F", + "defKey": "driver_id", + "defName": "司机 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "160899D7-1A2F-4AAA-B418-765E5984C42F", + "defKey": "driver_user_id", + "defName": "司机所属用户 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "42BDB31C-BB61-4302-B3A7-97726EDA9E03", + "defKey": "driver_name", + "defName": "司机姓名", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "B6450857-702B-4260-8189-6E10F2F5B849", + "defKey": "driver_phone", + "defName": "司机联系电话", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "E867EDED-9E3D-4EB0-9276-23BE8A3574CE", + "defKey": "truck_id", + "defName": "车辆 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "B1E3D7FD-2952-4AB9-BD83-8B0DF8E02E3A", + "defKey": "truck_license_plate", + "defName": "车牌号", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "63C17A2A-CD26-484F-8D21-1181144C9B88", + "defKey": "history_tare_weight", + "defName": "历史皮重", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "82CE20FA-7DF0-435E-9F3C-70A4A8F884F4", + "defKey": "org_id", + "defName": "身份主体 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "4B5EA64F-9E41-4218-973C-2F6BA697D969", + "defKey": "org_name", + "defName": "主体名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "91E23426-0995-4788-82A9-4D4A26B6B39F", + "defKey": "org_contacts", + "defName": "主体联系人姓名", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "FCCC7E24-9F94-422D-A90E-CBC1441CFD43", + "defKey": "org_phone", + "defName": "主体联系电话", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "E139A1BC-4A5F-405C-84BD-0963E8DC5914", + "defKey": "rough_weight", + "defName": "毛重", + "intro": "单位:千克", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "08DFE4CD-7BBD-469E-A4C2-59441BF72EAE", + "defKey": "tare_weight", + "defName": "皮重", + "intro": "单位:千克", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "83C2781E-6AEF-4C5E-A3AE-1A00E25E5AB5", + "defKey": "settle_weight", + "defName": "净重", + "intro": "单位:千克", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "A1F02DEE-877C-4FEA-AA24-2F7CE6CD12A7", + "defKey": "in_front_photo", + "defName": "进场车头照片", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "3FE031F0-2E22-4EDB-B440-9E54227DFD54", + "defKey": "in_body_photo", + "defName": "进场车斗照片", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "BE41C5D8-AE8D-431D-AC1D-C46571516721", + "defKey": "out_front_photo", + "defName": "出场车头照片", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "6178369D-7CDB-4645-ACAF-4FB7920DD907", + "defKey": "out_body_photo", + "defName": "出场车斗照片", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "020E3B61-5CE2-4900-999B-EDD1CB15A5B3", + "defKey": "in_time", + "defName": "进场时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "633D85AC-D560-464F-B8E2-F845B6DAAC18", + "defKey": "out_time", + "defName": "出场时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "4E26BE38-0359-4A18-B025-E996E574D7C8", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "11B7560B-19E1-4600-8B73-4990D27CD1D3", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "EDF1827D-51E8-45AD-98C6-4D57AACF57A8", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "DC3B494B-0F81-43DE-BBBE-84CE6D8C201F", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "2023B533-447D-4928-8256-5D9D9D0F14AF", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "F573B178-BE6E-48AB-8AF0-6121E7320EB9", + "schemaName": null, + "defKey": "cst_order_expense_items", + "defName": "付费项", + "intro": "", + "type": "P", + "fields": [ + { + "id": "97980599-17FA-46BF-A07B-01C97CFFA0D5", + "defKey": "id", + "defName": "Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 1, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "7509E84A-2C66-4FF7-AD9D-38A4EFD21C5C", + "defKey": "order_id", + "defName": "订单 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "82AF6F31-1944-42E9-B1E5-143AB1418DF6", + "defKey": "discount_money", + "defName": "优惠金额", + "intro": "有正负,单位:元", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "77E04850-F1DE-4500-A133-3A0CDB1F1A12", + "defKey": "settle_money", + "defName": "结算金额", + "intro": "单位:元", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "2519C28A-EF03-4D48-91A7-7AD6EB0EF434", + "defKey": "revise_money", + "defName": "手动修正金额", + "intro": "有正负,单位:元", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "0C5F1538-B5CF-435A-AF77-BEB2AC7CC249", + "defKey": "total_money", + "defName": "总金额", + "intro": "单位:元", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "35327184-50C2-4821-A55E-E028E23F6CB5", + "defKey": "payment_status", + "defName": "支付状态", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "payment_status", + "stndFieldId": "", + "stndDictKey": "payment_status", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "WeiZhiFu", + "itemName": "未支付", + "parentKey": "", + "intro": "", + "id": "4E544E5C-3A58-4EF3-B43E-9920D95C4850" + }, + { + "itemKey": "YiZhiFu", + "itemName": "已支付", + "parentKey": "", + "intro": "", + "id": "1FDE8C9C-AF01-4D10-8B81-BAD8B5D52EC9" + }, + { + "itemKey": "YiTuiKuan", + "itemName": "已退款", + "parentKey": "", + "intro": "", + "id": "CD40B918-2BCD-4E02-B911-F05BBD0ADBBC" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "C5201E2E-5DF1-4389-A97B-F8C14428E24A", + "defKey": "settlement_way", + "defName": "结算方式", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 32, + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "settlement_way", + "stndFieldId": "", + "stndDictKey": "settlement_way", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "YueJie", + "itemName": "月结", + "parentKey": "", + "intro": "", + "id": "21E8CE9A-A4E4-43EC-802F-C62332F485CE" + }, + { + "itemKey": "YuE", + "itemName": "余额", + "parentKey": "", + "intro": "", + "id": "1E3F7458-C19E-481F-A6BB-881CB6E576CD" + }, + { + "itemKey": "XianFu", + "itemName": "现付", + "parentKey": "", + "intro": "", + "id": "02F37949-4DCD-4BF8-93DB-76D974A1BC47" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "C85B2E69-CB69-4505-802D-D87D77221B28", + "defKey": "payer_id", + "defName": "付款人 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "3746CD2C-A17E-415E-95FA-9F88DFB6285C", + "defKey": "payer_money_account", + "defName": "付款方资金账户 Id", + "intro": "", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "74DAB22A-CDFD-445D-9C15-E32211E5A9CF", + "defKey": "payment_time", + "defName": "支付时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 0, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "UI" + }, + { + "id": "568436FE-1772-4E0B-8AEF-81F6F806A5BF", + "defKey": "expense_item_category", + "defName": "收费项目类型", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "expense_item_category", + "stndFieldId": "", + "stndDictKey": "expense_item_category", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "QiTa", + "itemName": "其他", + "parentKey": "", + "intro": "", + "id": "B4560F4E-C17C-45E6-BA46-F24447710D73" + }, + { + "itemKey": "ChanPin", + "itemName": "产品", + "parentKey": "", + "intro": "", + "id": "1078D9EB-D7B7-4AE4-BB59-C5981DFFE609" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "850B4B65-409F-4F25-AFA0-9ABDE60AD557", + "defKey": "expense_item_name", + "defName": "付费项名称", + "intro": "", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 128, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "B928469A-6CCE-4E8B-9656-B561F67396AF", + "defKey": "expense_strategy", + "defName": "计费策略", + "intro": "字典代码:expense_strategy", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 64, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "expense_strategy", + "stndFieldId": "", + "stndDictKey": "expense_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "MianFei", + "itemName": "免费", + "parentKey": "", + "intro": "", + "id": "7B74209A-E0F9-4D7E-B437-A15802FAC11B" + }, + { + "itemKey": "TanXing", + "itemName": "弹性", + "parentKey": "", + "intro": "", + "id": "B247787C-6787-41E8-BADD-5F155C62B7A9" + }, + { + "itemKey": "Juli", + "itemName": "按车", + "parentKey": "", + "intro": "", + "id": "34D2FFAC-CB4B-4E3B-8125-6605491E95A8" + }, + { + "itemKey": "Dun", + "itemName": "按吨", + "parentKey": "", + "intro": "", + "id": "17C1DC4B-B707-4DAD-8B24-484838F9C157" + }, + { + "itemKey": "Che", + "itemName": "按方", + "parentKey": "", + "intro": "", + "id": "CC0058F6-02CA-4645-9FEF-9143833C2964" + }, + { + "itemKey": "Fang", + "itemName": "按距离", + "parentKey": "", + "intro": "", + "id": "024EEBD1-5C0E-4AA4-A5EA-3E2BAA80628A" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "95984ED8-26A5-437F-8004-C5BDAD3B7E9A", + "defKey": "tax_rate", + "defName": "税率", + "intro": "", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 10, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "0C8BF917-7A07-4EE4-8575-2DFA5EB41EF7", + "defKey": "payer", + "defName": "付费方", + "intro": "字典代码:payer", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "payer", + "stndFieldId": "", + "stndDictKey": "payer", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "PingTai", + "itemName": "平台", + "parentKey": "", + "intro": "", + "id": "F9E5B2B3-3E58-4B22-88F6-C3925BDD4EFF" + }, + { + "itemKey": "ChanFei", + "itemName": "产废方", + "parentKey": "", + "intro": "", + "id": "DAE6A790-C2D0-4972-9491-FEAF67E5B00C" + }, + { + "itemKey": "QingYun", + "itemName": "清运方", + "parentKey": "", + "intro": "", + "id": "BDD02160-F9E7-4E0B-A227-008E0E0DCBB9" + }, + { + "itemKey": "XiaoNa", + "itemName": "消纳方", + "parentKey": "", + "intro": "", + "id": "19C7D008-74BD-4478-8705-D5EB03C54DA5" + }, + { + "itemKey": "CaiGou", + "itemName": "采购方", + "parentKey": "", + "intro": "", + "id": "0EF79E9D-AF63-43B5-A7CB-645CA1C01597" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "113F9CA5-9A9F-49DC-B3CF-B63045343006", + "defKey": "unit", + "defName": "计量单位", + "intro": "字典代码:unit", + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 16, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "555713D6-B1B4-4548-9174-1574B26DFC86", + "defKey": "unit_price", + "defName": "单价", + "intro": "单位:元,弹性模式-->每档价格", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 16, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0.0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "38ED21B8-8065-43F8-A461-9EE42FE0CDAA", + "defKey": "initial_price", + "defName": "起步价", + "intro": "单位:元,<= 起步量 固定费用", + "baseDataType": "DECIMAL", + "bizDomainType": "", + "dbDataType": "DECIMAL", + "dataLen": 16, + "numScale": 6, + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0.0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "FC4CD2D4-ACA6-407C-AE94-60803F0ED9D1", + "defKey": "initial_quantity", + "defName": "起步量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "45B5919A-0B50-4860-88E1-6AB3E5614EEE", + "defKey": "every_quantity", + "defName": "每档的量", + "intro": "", + "baseDataType": "INT", + "bizDomainType": "", + "dbDataType": "INT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3A320857-94C6-46AE-BE07-823A7C4F581F", + "defKey": "user_scope", + "defName": "适用用户", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "4270C166-11F1-4420-BA9A-31449DCED60E", + "defKey": "station_scope", + "defName": "适用站点", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "9A55E6F2-B165-4788-A6BA-00FE644E4CF6", + "defKey": "goods_scope", + "defName": "适用产品", + "intro": "结构类型:{\n strategy: None | All | Specify,\n objs: long[]\n}", + "baseDataType": "TEXT", + "bizDomainType": "", + "dbDataType": "TEXT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "scope_strategy", + "stndFieldId": "", + "stndDictKey": "scope_strategy", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "Manual", + "dictItems": [ + { + "itemKey": "Wu", + "itemName": "无", + "parentKey": "", + "intro": "", + "id": "5C25320D-C9AA-4AF6-B1DD-09363617D653" + }, + { + "itemKey": "ZhiDing", + "itemName": "指定", + "parentKey": "", + "intro": "", + "id": "24FE3E6E-E3F6-4C2C-AE0D-F77C2D6754A4" + }, + { + "itemKey": "SuoYou", + "itemName": "所有", + "parentKey": "", + "intro": "", + "id": "72531125-9D71-47CA-8396-22F004CB77B0" + } + ], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "7EF2F922-5898-49D1-AA9B-EB4E0E633D7B", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "B24CC9A5-546D-4CBE-895A-B1110D4DEC56", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": "sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3C80AA09-DB48-4D36-A8DB-464E14BD57A2", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "0C8D8BBF-BC1A-477D-A217-892DACF7C78E", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "EAEFF405-6D0D-48AE-BA7E-2C917BFA8F7B", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "indexes": [] + }, + { + "id": "DF643AC9-1A78-408B-9F20-9C659BC475FA", + "type": "P", + "defKey": "cst_driver", + "defName": "司机", + "intro": null, + "schemaName": null, + "props": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "fields": [ + { + "id": "EA102029-949A-445E-BA8F-BD59EF57DF29", + "defKey": "id", + "defName": "Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": 1, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "114C1B4D-4FCA-4E61-BE22-9EC93DD09B8D", + "defKey": "user_id", + "defName": "归属用户 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "4CFA28DA-7FEC-4801-AF24-E0EC059880FF", + "defKey": "org_id", + "defName": "归属公司 Id", + "intro": null, + "orderValue": null, + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D3A9A734-B32F-4861-894B-639D571C1909", + "defKey": "driving_licence_no", + "defName": "驾驶证编号", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 30, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "D1F18BDE-577C-4129-977C-CF50C0B92147", + "defKey": "driver_name", + "defName": "司机姓名", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "B37E5A7C-05D3-47E7-A942-3CC7FD028B07", + "defKey": "phone", + "defName": "手机号", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 20, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "7610FE82-69E1-4E7D-A58A-7DC7ECAD3F1C", + "defKey": "driving_licence", + "defName": "驾驶证图片", + "intro": null, + "orderValue": null, + "baseDataType": "VARCHAR", + "bizDomainType": "", + "dbDataType": "VARCHAR", + "dataLen": 255, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "659318AE-136A-4E95-B749-F02D642A8A56", + "defKey": "licence_start_time", + "defName": "驾驶证有效期", + "intro": null, + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 0, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "E901B6BF-3C66-4E33-8B2D-9A408C2417B6", + "defKey": "licence_end_time", + "defName": "驾驶证有效期", + "intro": null, + "orderValue": null, + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": null, + "numScale": null, + "primaryKey": null, + "notNull": 0, + "autoIncrement": null, + "defaultValue": null, + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "9AF0B3F7-F1F2-4183-8DEE-4078375B8F40", + "defKey": "busy", + "defName": "忙碌中", + "intro": null, + "orderValue": null, + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": null, + "primaryKey": null, + "notNull": 1, + "autoIncrement": null, + "defaultValue": "0", + "stndDictId": null, + "stndDictKey": null, + "stndFieldId": null, + "stndFieldKey": null, + "mark": null, + "attr1": null, + "attr2": null, + "attr3": null, + "attr4": null, + "attr5": null, + "attr6": null, + "attr7": null, + "attr8": null, + "attr9": null, + "attr10": null, + "attr11": null, + "attr12": null, + "attr13": null, + "attr14": null, + "attr15": null, + "attr16": null, + "attr17": null, + "attr18": null, + "attr19": null, + "attr20": null, + "origin": null, + "stndComplianceType": null, + "stndComplianceLevel": null, + "updatedUserId": null, + "createdUserId": null + }, + { + "id": "F45BFDF4-AF71-4F59-B661-80C4C21A6DF6", + "defKey": "creator_id", + "defName": "创建人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": null, + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "4B5C06B5-164F-412E-A8EF-8450517760D3", + "defKey": "modifier_id", + "defName": "修改人 Id", + "intro": " sys_user.id", + "baseDataType": "BIGINT", + "bizDomainType": "", + "dbDataType": "BIGINT", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "B1FFB331-E210-45B5-9C46-B6619943B9E0", + "defKey": "create_time", + "defName": "创建时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "3FB9D047-6ECB-4EE2-A383-F418D62617B2", + "defKey": "modify_time", + "defName": "修改时间", + "intro": "", + "baseDataType": "DATETIME", + "bizDomainType": "", + "dbDataType": "DATETIME", + "dataLen": "", + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + }, + { + "id": "1AAB5119-A10C-4740-BEFA-5F92F02873AC", + "defKey": "deleted", + "defName": "是否删除", + "intro": " 0-->未删除、1-->已删除", + "baseDataType": "TINYINT", + "bizDomainType": "", + "dbDataType": "TINYINT", + "dataLen": 1, + "numScale": "", + "primaryKey": 0, + "notNull": 1, + "autoIncrement": 0, + "defaultValue": "0", + "stndDictId": "", + "stndFieldId": "", + "stndDictKey": "", + "stndFieldKey": "", + "stndComplianceLevel": "", + "stndComplianceType": "", + "dictFrom": "", + "dictItems": [], + "fieldTier": "", + "mark": null, + "attr1": "", + "attr2": "", + "attr3": "", + "attr4": "", + "attr5": "", + "attr6": "", + "attr7": "", + "attr8": "", + "attr9": "", + "attr10": "", + "attr11": "", + "attr12": "", + "attr13": "", + "attr14": "", + "attr15": "", + "attr16": "", + "attr17": "", + "attr18": "PDManer", + "attr19": "", + "attr20": "", + "origin": "PASTE" + } + ], + "correlations": null, + "indexes": [] } ], "diagrams": [], "readonly": false, "allowWs": false }, - "updateTime": 1764576011193, - "signature": "5a04596e8231748f7af750043551f42e", + "updateTime": 1765033540112, + "signature": "912e4a9f4d6f7d13555170cb51455708", "branchId": "1111" } \ No newline at end of file