'init-again'
Change-Id: Ic0ec9c96098897288508eddd75f89a0f4fa6e986
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..14ca66d
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: https://gitee.com/whiteshader/ruoyi-react
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed8368a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+######################################################################
+# Build Tools
+
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### JRebel ###
+rebel.xml
+
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3541dcd
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "files.exclude": {
+ "**/.classpath": true,
+ "**/.project": true,
+ "**/.settings": true,
+ "**/.factorypath": true
+ },
+ "java.compile.nullAnalysis.mode": "automatic",
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67430c7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+ThunderHub
+基于BT下载协议的私有电子资源分享平台
+已经配置了mybatis-plus
+
+## 运行说明
+1. 导入maven,加载pom.xml
+2. 启动redis (下载后,双击redis-serve.exe)
+3. 点击启动
\ No newline at end of file
diff --git a/Redis-x64-5.0.14.1/00-RELEASENOTES b/Redis-x64-5.0.14.1/00-RELEASENOTES
new file mode 100644
index 0000000..21b4375
--- /dev/null
+++ b/Redis-x64-5.0.14.1/00-RELEASENOTES
@@ -0,0 +1,3630 @@
+Redis 5.0 release notes
+=======================
+
+--------------------------------------------------------------------------------
+Upgrade urgency levels:
+
+LOW: No need to upgrade unless there are new features you want to use.
+MODERATE: Program an upgrade of the server, but it's not urgent.
+HIGH: There is a critical bug that may affect a subset of users. Upgrade!
+CRITICAL: There is a critical bug affecting MOST USERS. Upgrade ASAP.
+SECURITY: There are security fixes in the release.
+--------------------------------------------------------------------------------
+
+================================================================================
+Redis 5.0.14 Released Mon Oct 4 12:00:00 IDT 2021
+================================================================================
+
+Upgrade urgency: SECURITY, contains fixes to security issues.
+
+Security Fixes:
+* (CVE-2021-41099) Integer to heap buffer overflow handling certain string
+ commands and network payloads, when proto-max-bulk-len is manually configured
+ to a non-default, very large value [reported by yiyuaner].
+* (CVE-2021-32762) Integer to heap buffer overflow issue in redis-cli and
+ redis-sentinel parsing large multi-bulk replies on some older and less common
+ platforms [reported by Microsoft Vulnerability Research].
+* (CVE-2021-32687) Integer to heap buffer overflow with intsets, when
+ set-max-intset-entries is manually configured to a non-default, very large
+ value [reported by Pawel Wieczorkiewicz, AWS].
+* (CVE-2021-32675) Denial Of Service when processing RESP request payloads with
+ a large number of elements on many connections.
+* (CVE-2021-32672) Random heap reading issue with Lua Debugger [reported by
+ Meir Shpilraien].
+* (CVE-2021-32628) Integer to heap buffer overflow handling ziplist-encoded
+ data types, when configuring a large, non-default value for
+ hash-max-ziplist-entries, hash-max-ziplist-value, zset-max-ziplist-entries
+ or zset-max-ziplist-value [reported by sundb].
+* (CVE-2021-32627) Integer to heap buffer overflow issue with streams, when
+ configuring a non-default, large value for proto-max-bulk-len and
+ client-query-buffer-limit [reported by sundb].
+* (CVE-2021-32626) Specially crafted Lua scripts may result with Heap buffer
+ overflow [reported by Meir Shpilraien].
+
+================================================================================
+Redis 5.0.13 Released Wed Jul 21 16:32:19 IDT 2021
+================================================================================
+
+Upgrade urgency: SECURITY, contains fixes to security issues that affect
+authenticated client connections on 32-bit versions. MODERATE otherwise.
+
+Fix integer overflow in BITFIELD on 32-bit versions (CVE-2021-32761).
+An integer overflow bug in Redis version 2.2 or newer can be exploited using the
+BITFIELD command to corrupt the heap and potentially result with remote code
+execution.
+
+Bug fixes:
+* Fix overflows on 32-bit versions in GETBIT, SETBIT, BITCOUNT, BITPOS, and BITFIELD (#9191)
+* Fix ziplist length updates on big-endian platforms (#2080)
+
+================================================================================
+Redis 5.0.12 Released Mon Mar 1 17:29:52 IST 2021
+================================================================================
+
+Upgrade urgency: LOW, fixes a compilation issue.
+
+Bug fixes:
+* Fix compilation error on non-glibc systems if jemalloc is not used (#8533)
+
+================================================================================
+Redis 5.0.11 Released Mon Feb 22 16:48:25 IST 2021
+================================================================================
+
+Upgrade urgency: SECURITY if you use 32bit build of redis (see bellow), LOW
+otherwise.
+
+Integer overflow on 32-bit systems (CVE-2021-21309):
+Redis 4.0 or newer uses a configurable limit for the maximum supported bulk
+input size. By default, it is 512MB which is a safe value for all platforms.
+If the limit is significantly increased, receiving a large request from a client
+may trigger several integer overflow scenarios, which would result with buffer
+overflow and heap corruption.
+
+Bug fixes:
+* Avoid 32-bit overflows when proto-max-bulk-len is set high (#8522)
+* Fix an issue where a forked process deletes the parent's pidfile (#8231)
+* Fix flock cluster config may cause failure to restart after kill -9 (#7674)
+* Avoid an out-of-bounds read in the redis-sentinel (#7443)
+
+Platform and deployment-related changes:
+* Fix setproctitle related crashes. (#8150, #8088)
+ Caused various crashes on startup, mainly on Apple M1 chips or under
+ instrumentation.
+* Add a check for an ARM64 Linux kernel bug (#8224)
+ Due to the potential severity of this issue, Redis will refuse to run on
+ affected platforms by default.
+
+Modules:
+* RM_ZsetRem: Delete key if empty, the bug could leave empty zset keys (#8453)
+
+================================================================================
+Redis 5.0.10 Released Mon Oct 26 09:21:49 IST 2020
+================================================================================
+
+Upgrade urgency: SECURITY if you use an affected platform (see below).
+ Otherwise the upgrade urgency is MODERATE.
+
+This release fixes a potential heap overflow when using a heap allocator other
+than jemalloc or glibc's malloc. See:
+https://github.com/redis/redis/pull/7963
+
+Other fixes in this release:
+
+* Avoid case of Lua scripts being consistently aborted due to OOM
+* XPENDING will not update consumer's seen-time
+* A blocked XREADGROUP didn't propagated the XSETID to replicas / AOF
+* UNLINK support for streams
+* RESTORE ABSTTL won't store expired keys into the DB
+* Hide AUTH from MONITOR
+* Cluster: reduce spurious PFAIL/FAIL states upon delayed PONG receival
+* Cluster: Fix case of clusters mixing accidentally by gossip
+* Cluster: Allow blocked XREAD on a cluster replica
+* Cluster: Optimize memory usage CLUSTER SLOTS command
+* RedisModule_ValueLength support for stream data type
+* Minor fixes in redis-check-rdb and redis-cli
+* Fix redis-check-rdb support for modules aux data
+* Add fsync in replica when full RDB payload was received
+
+Full list of commits:
+
+Yossi Gottlieb in commit ce0d74d8f:
+ Fix wrong zmalloc_size() assumption. (#7963)
+ 1 file changed, 3 deletions(-)
+
+Yossi Gottlieb in commit 066699240:
+ Backport Lua 5.2.2 stack overflow fix. (#7733)
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+WuYunlong in commit 8a90c7ef3:
+ Add fsync to readSyncBulkPayload(). (#7839)
+ 1 file changed, 11 insertions(+)
+
+Ariel Shtul in commit f0df2bb3c:
+ Fix redis-check-rdb support for modules aux data (#7826)
+ 3 files changed, 21 insertions(+), 1 deletion(-)
+
+hwware in commit 7add2a412:
+ fix memory leak in sentinel connection sharing
+ 1 file changed, 1 insertion(+)
+
+Oran Agra in commit 315e648f8:
+ Allow blocked XREAD on a cluster replica (#7881)
+ 3 files changed, 100 insertions(+), 2 deletions(-)
+
+guybe7 in commit 4967ee94e:
+ Modules: Invalidate saved_oparray after use (#7688)
+ 1 file changed, 2 insertions(+)
+
+antirez in commit 065003e8f:
+ Modules: remove spurious call from moduleHandleBlockedClients().
+ 1 file changed, 1 deletion(-)
+
+Angus Pearson in commit 6cdf62928:
+ Fix broken interval and repeat bahaviour in redis-cli (incluing cluster mode)
+ 1 file changed, 11 insertions(+), 6 deletions(-)
+
+antirez in commit cb6a4971c:
+ Cluster: introduce data_received field.
+ 2 files changed, 27 insertions(+), 10 deletions(-)
+
+Madelyn Olson in commit 83f4de865:
+ Hide AUTH from monitor
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Guy Benoish in commit 3ba08d185:
+ Support streams in general module API functions
+ 3 files changed, 11 insertions(+), 1 deletion(-)
+
+Itamar Haber in commit 109c0635c:
+ Expands lazyfree's effort estimate to include Streams (#5794)
+ 1 file changed, 24 insertions(+)
+
+huangzhw in commit 235210d5b:
+ defrag.c activeDefragSdsListAndDict when defrag sdsele, We can't use (#7492)
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit fdd3162fe:
+ RESTORE ABSTTL skip expired keys - leak (#7511)
+ 1 file changed, 1 insertion(+)
+
+Oran Agra in commit 6139d6d18:
+ RESTORE ABSTTL won't store expired keys into the db (#7472)
+ 4 files changed, 45 insertions(+), 15 deletions(-)
+
+Liu Zhen in commit 0f502c58d:
+ fix clusters mixing accidentally by gossip
+ 1 file changed, 10 insertions(+), 2 deletions(-)
+
+Guy Benoish in commit 37fd50718:
+ XPENDING should not update consumer's seen-time
+ 4 files changed, 30 insertions(+), 18 deletions(-)
+
+antirez in commit a3ca53e4a:
+ Also use propagate() in streamPropagateGroupID().
+ 1 file changed, 11 insertions(+), 1 deletion(-)
+
+yanhui13 in commit 7a62eb96e:
+ optimize the output of cluster slots
+ 1 file changed, 7 insertions(+), 4 deletions(-)
+
+srzhao in commit 0efb93d0c:
+ Check OOM at script start to get stable lua OOM state.
+ 3 files changed, 11 insertions(+), 4 deletions(-)
+
+================================================================================
+Redis 5.0.9 Released Thu Apr 17 12:41:00 CET 2020
+================================================================================
+
+Upgrade urgency:CRITICAL if you use Streams with AOF ore replicas.
+ Otherwise the upgrade urgency is LOW.
+
+This release has a speed improvement and a critical fix:
+
+ * FIX: XREADGROUP when fetching data in a blocking way, would not
+ emit the XCLAIM in the AOF file and to replicas. This means
+ that the last ID is not updated, and that restarting the server
+ will have the effect of reprocessing some entries.
+ * NEW: Clients blocked on the same key are now unblocked on
+ O(1) time. Backported from Redis 6.
+
+Commits:
+
+ 1fc8ef81a Fix XCLAIM propagation in AOF/replicas for blocking XREADGROUP.
+ a5e24eabc Speedup: unblock clients on keys in O(1).
+
+================================================================================
+Redis 5.0.8 Released Thu Mar 12 16:05:41 CET 2020
+================================================================================
+
+Upgrade urgency HIGH: This release fixes security issues.
+
+This is a list of fixes in this release:
+
+Salvatore Sanfilippo in commit 2bea502d:
+ Merge pull request #6975 from dustinmm80/add-arm-latomic-linking
+Dustin Collins in commit b5931405:
+ Fix Pi building needing -latomic, backport
+ 1 file changed, 9 insertions(+)
+
+srzhao in commit fd441300:
+ fix impl of aof-child whitelist SIGUSR1 feature.
+ 1 file changed, 5 insertions(+), 4 deletions(-)
+
+Ariel in commit 77ff332b:
+ fix ThreadSafeContext lock/unlock function names
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Guy Benoish in commit 4f0f799c:
+ XREADGROUP should propagate XCALIM/SETID in MULTI/EXEC
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Oran Agra in commit 0c1273c3:
+ Fix client flags to be int64 in module.c
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Guy Benoish in commit 708a4e8a:
+ Fix small bugs related to replica and monitor ambiguity
+ 2 files changed, 8 insertions(+), 6 deletions(-)
+
+WuYunlong in commit eac4115d:
+ Fix lua related memory leak.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit d075df17:
+ Simplify #6379 changes.
+ 2 files changed, 4 insertions(+), 9 deletions(-)
+
+WuYunlong in commit 80a49c37:
+ Free allocated sds in pfdebugCommand() to avoid memory leak.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit 60870d3a:
+ Jump to right label on AOF parsing error.
+ 1 file changed, 6 insertions(+), 4 deletions(-)
+
+antirez in commit d90f599b:
+ Free fakeclient argv on AOF error.
+ 1 file changed, 11 insertions(+), 3 deletions(-)
+
+WuYunlong in commit 8ee3bddf:
+ Fix potential memory leak of rioWriteBulkStreamID().
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+WuYunlong in commit 4780fe78:
+ Fix potential memory leak of clusterLoadConfig().
+ 1 file changed, 20 insertions(+), 5 deletions(-)
+
+Leo Murillo in commit f3b77510:
+ Fix bug on KEYS command where pattern starts with * followed by \x00 (null char).
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Guy Benoish in commit 7f3fcedb:
+ Blocking XREAD[GROUP] should always reply with valid data (or timeout)
+ 3 files changed, 44 insertions(+), 10 deletions(-)
+
+antirez in commit f93b2fa5:
+ XCLAIM: Create the consumer only on successful claims.
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+Guy Benoish in commit 89682d96:
+ Stream: Handle streamID-related edge cases
+ 4 files changed, 54 insertions(+), 4 deletions(-)
+
+antirez in commit 920e108f:
+ Fix ip and missing mode in RM_GetClusterNodeInfo().
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+antirez in commit 7569b210:
+ Inline protocol: handle empty strings well.
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+Khem Raj in commit 3c610b4e:
+ Mark extern definition of SDS_NOINIT in sds.h
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Seunghoon Woo in commit 16b2d07f:
+ [FIX] revisit CVE-2015-8080 vulnerability
+ 1 file changed, 6 insertions(+), 4 deletions(-)
+
+yz1509 in commit 19f33585:
+ avoid sentinel changes promoted_slave to be its own replica.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0.7 Released Tue Nov 19 17:52:44 CET 2019
+================================================================================
+
+Upgrade urgency HIGH: many issues fixed, some may have an impact.
+
+Hi all, Redis 5.0.7 fixes a number of bugs, none is very critical, however
+there are a few that may have an impact. It's a good idea to upgrade.
+There are fixes in the area of replication from modules commands and
+callbacks, AOF fsync (non critical issue), memory leaks (very rare and small),
+streams beahvior (non critical), and a potential crash in commands
+processing multiple keys at the same time that is there for years, and happens
+very rarely, but is not impossible to trigger.
+
+The following is the list of commits in this release.
+
+antirez in commit 4d2a31ae:
+ Test: fix implementation-dependent test after code change.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Oran Agra in commit 9f63fc98:
+ RED-31295 - redis: avoid race between dlopen and thread creation
+ 2 files changed, 17 insertions(+)
+
+antirez in commit 1a9e70c1:
+ Cluster: fix memory leak of cached master.
+ 2 files changed, 9 insertions(+), 4 deletions(-)
+
+Guy Benoish in commit 69b1b5be:
+ Fix usage of server.stream_node_max_*
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+喜欢兰花山丘 in commit 1fd97ee7:
+ Update mkreleasehdr.sh
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 1a9855d7:
+ Remove additional space from comment.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 32a6e3e4:
+ Fix stream test after addition of 0-0 ID test.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Yuan Zhou in commit c9e6cda9:
+ aof: fix assignment for aof_fsync_offset
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit d3eeacf9:
+ Merge branch '5.0' of github.com:/antirez/redis into 5.0
+antirez in commit da5dc458:
+ Rename var to fixed_time_expire now that is more general.
+ 4 files changed, 7 insertions(+), 8 deletions(-)
+
+antirez in commit 0fefed25:
+ Fix patch provided in #6554.
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit e9fbc960:
+ expires & blocking: handle ready keys as call()
+ 1 file changed, 5 insertions(+)
+
+Guy Benoish in commit 08ec8f71:
+ XADD with ID 0-0 stores an empty key
+ 2 files changed, 14 insertions(+)
+
+Loris Cro in commit 09e1fe27:
+ fix unreported overflow in autogerenared stream IDs
+ 2 files changed, 19 insertions(+), 10 deletions(-)
+
+Salvatore Sanfilippo in commit 09f9e4b0:
+ Merge pull request #6600 from oranagra/5_module_flags
+Oran Agra in commit 8d8d68dd:
+ module documentation mismatches: loading and fork child for 5.0 branch
+ 3 files changed, 11 insertions(+)
+
+antirez in commit 7a7fbe70:
+ Modules: RM_GetContextFlags(): remove non Redis 5 features.
+ 1 file changed, 3 deletions(-)
+
+antirez in commit b5830486:
+ Modules: fix moduleCreateArgvFromUserFormat() casting bug.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit b7a2a53a:
+ module: fix propagation API bug.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 278bd6e3:
+ Modules: add new flags to context, replica state + more.
+ 2 files changed, 48 insertions(+), 1 deletion(-)
+
+antirez in commit 61d9a154:
+ Modules: RM_Call(): give pointer to documentation.
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+antirez in commit 0e7ea0aa:
+ Modules: RM_Call/Replicate() ability to exclude AOF/replicas.
+ 1 file changed, 28 insertions(+), 9 deletions(-)
+
+antirez in commit 3b38164e:
+ Modules: RM_Replicate() in thread safe contexts.
+ 1 file changed, 26 insertions(+), 4 deletions(-)
+
+antirez in commit ef9fe9b0:
+ Modules: implement RM_Replicate() from async callbacks.
+ 2 files changed, 33 insertions(+), 2 deletions(-)
+
+antirez in commit 8066d2a1:
+ Modules: handle propagation when ctx is freed. Flag modules commands ctx.
+ 1 file changed, 23 insertions(+), 19 deletions(-)
+
+antirez in commit d3f4dec4:
+ Update PR #6537: use a fresh time outside call().
+ 3 files changed, 27 insertions(+), 7 deletions(-)
+
+antirez in commit 33f42665:
+ Update PR #6537 patch to for generality.
+ 4 files changed, 40 insertions(+), 23 deletions(-)
+
+zhaozhao.zz in commit 68d71d83:
+ expires: refactoring judgment about whether a key is expired
+ 3 files changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit 62588dbf:
+ Modules: fix thread safe context creation crash.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0.6 Released Wed Sep 25 12:33:56 CEST 2019
+================================================================================
+
+Upgrade urgency CRITICAL: Only in case of exposed instances to untrusted users.
+
+This Redis release, 5.0.6, is a bugfix and enhancement release. The most
+important bugfix is a corruption related to the HyperLogLog. A malformed
+HyperLogLog string could cause an invalid access to the memory. At a first
+glance the vulnerability appears to be not exploitable but just a DoS. The
+way to trigger the issue is complex, we'll not provide any information about
+how to do that for the users safety.
+
+Other significant changes in this release:
+
+* New modules APIs merged from Redis unstable to Redis 5.
+* Some memory optimization related to objects creation.
+* Fixes to flushSlaveOutputBuffer() that make sure that SHUTDOWN will
+ transfer pending buffers to replicas.
+
+This is the full list of commits:
+
+antirez in commit 7a41047a:
+ RDB: fix MODULE_AUX loading by continuing to next opcode.
+ 1 file changed, 1 insertion(+)
+
+Oran Agra in commit 4eb3028b:
+ missing per-skiplist overheads in MEMORY USAGE
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+Oran Agra in commit 5d09f9bc:
+ RM_Log - add support for logging without a context or context without module
+ 1 file changed, 6 insertions(+), 4 deletions(-)
+
+antirez in commit 2810de9f:
+ Cluster: abort loading nodes data if vars arguments are unbalanced.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit f5c63ce0:
+ More strict checks and better comments in flushSlaveOutputBuffers().
+ 1 file changed, 18 insertions(+), 7 deletions(-)
+
+antirez in commit 7f289c3b:
+ Improve comment in flushSlavesOutputBuffers().
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit 7ab62d4b:
+ Replication: clarify why repl_put_online_on_ack exists at all.
+ 2 files changed, 34 insertions(+), 10 deletions(-)
+
+zhaozhao.zz in commit 495dd0da:
+ networking: flushSlavesOutputBuffers bugfix
+ 1 file changed, 2 insertions(+), 4 deletions(-)
+
+Salvatore Sanfilippo in commit c1ccf0f1:
+ Merge pull request #6366 from oranagra/5.0_rm_reply_cstring
+Salvatore Sanfilippo in commit a50dad73:
+ Merge pull request #6365 from oranagra/5.0_module_aux
+Oran Agra in commit d6294d05:
+ RM_ReplyWithCString was missing registration
+ 1 file changed, 1 insertion(+)
+
+Oran Agra in commit 8c56fc86:
+ Fix to module aux data rdb format for backwards compatibility with old check-rdb
+ 1 file changed, 9 insertions(+), 1 deletion(-)
+
+Oran Agra in commit 98b1314f:
+ Implement module api for aux data in rdb
+ 9 files changed, 431 insertions(+), 18 deletions(-)
+
+antirez in commit 08b03e23:
+ redis-cli: always report server errors on read errors.
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+wubostc in commit 239069de:
+ Reduce the calling stack
+ 1 file changed, 2 insertions(+), 3 deletions(-)
+
+antirez in commit 90bf6313:
+ Make EMBSTR case of #6261 more obvious.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+chendianqiang in commit 2f8a0749:
+ make memory usage consistent of robj with OBJ_ENCODING_INT
+ 1 file changed, 9 insertions(+), 4 deletions(-)
+
+antirez in commit 436ed56d:
+ HyperLogLog: fix the fix of a corruption bug.
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+John Sully in commit 680f89fb:
+ Fix HLL corruption bug
+ 1 file changed, 1 insertion(+)
+
+swilly22 in commit 388efbf8:
+ Extend REDISMODULE_CTX_FLAGS to indicate if redis is currently loading from either RDB or AOF
+ 2 files changed, 5 insertions(+)
+
+Itamar Haber in commit 0ccbdcee:
+ Uses addReplyBulkCString
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Itamar Haber in commit 707a59c6:
+ Adds RedisModule_ReplyWithCString
+ 2 files changed, 13 insertions(+)
+
+================================================================================
+Redis 5.0.5 Released Wed May 15 17:57:41 CEST 2019
+================================================================================
+
+Upgrade urgency CRITICAL: This release fixes an important AOF fysnc bug
+ and other less critical issues.
+
+
+Dear user,
+
+Redis 5.0.5 fixes an important issue with AOF and adds multiple very useful
+modules APIs. Moreover smaller bugs in other parts of Redis are fixed in
+this release.
+
+The AOF bug
+-----------
+
+The AOF bug happens when the fsync policy is set to "everysec", which is the
+default: if the write load in the server drops immediately, the commands
+executed in the latest second may not be fsync-ed to disk as it should.
+This may lead to data loss in case the write load drops immediately and
+successively a server crash happens.
+
+Other things in this release
+----------------------------
+
+* Streams: a bug in the iterator could prevent certain items to be returned in
+ range queries under specific conditions.
+* Memleak in bitfieldCommand fixed.
+* Modules API: Preserve client->id for blocked clients.
+* Fix memory leak when rewriting config file in case of write errors.
+* New modules API: RedisModule_GetKeyNameFromIO().
+* Fix non critical bugs in diskless replication.
+* New mdouels API: command filtering. See RedisModule_RegisterCommandFilter();
+* Tests improved to be more deterministic.
+* Fix a Redis Cluster bug, manual failover may abort because of the master
+ sending PINGs to the replicas.
+
+The following is the full list of commmits.
+
+Regards,
+Salvatore
+
+Christian Zeller in commit 1cac9b4b:
+ Typo fixes in CONTRIBUTING
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit f63c5c7b:
+ Update CONTRIBUTING with present info.
+ 1 file changed, 15 insertions(+), 5 deletions(-)
+
+antirez in commit 668661da:
+ Narrow the effects of PR #6029 to the exact state.
+ 1 file changed, 17 insertions(+), 5 deletions(-)
+
+chendianqiang in commit 3c2800e3:
+ stop ping when client pause
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 7ac7ffd5:
+ Test: fix slowlog test false positive.
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit cc101721:
+ Make comment in getClientOutputBufferMemoryUsage() describing the present.
+ 1 file changed, 1 insertion(+), 8 deletions(-)
+
+WuYunlong in commit 72420b0d:
+ Do not active expire keys in the background when the switch is off.
+ 1 file changed, 6 insertions(+), 4 deletions(-)
+
+liaotonglang in commit 33a50d24:
+ delete sdsTest() from REDIS_TEST
+ 1 file changed, 2 deletions(-)
+
+zhaozhao.zz in commit 6a92836f:
+ test cases: skiptill -> skip-till
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit f179f71e:
+ make replication tests more stable on slow machines
+ 3 files changed, 34 insertions(+), 4 deletions(-)
+
+Yossi Gottlieb in commit 1825a4ec:
+ Add runtest-moduleapi with commandfilter coverage.
+ 5 files changed, 63 insertions(+), 28 deletions(-)
+
+Yossi Gottlieb in commit 9d20fdb4:
+ fix: missing initialization.
+ 3 files changed, 1 insertion(+)
+
+antirez in commit ded1980e:
+ Test: disable module testing for now.
+ 1 file changed, 1 deletion(-)
+
+Yossi Gottlieb in commit c3df78c2:
+ CommandFilter API: REDISMODULE_CMDFILTER_NOSELF.
+ 4 files changed, 62 insertions(+), 15 deletions(-)
+
+Yossi Gottlieb in commit 8d38ef20:
+ CommandFilter API: fix UnregisterCommandFilter.
+ 1 file changed, 2 insertions(+), 3 deletions(-)
+
+Yossi Gottlieb in commit 9b7009b1:
+ CommandFilter API: Add unregister option.
+ 4 files changed, 126 insertions(+), 32 deletions(-)
+
+Yossi Gottlieb in commit 05802442:
+ CommandFilter API: Extend documentation.
+ 1 file changed, 43 insertions(+), 5 deletions(-)
+
+Yossi Gottlieb in commit d5194daf:
+ CommandFilter API: hellofilter and tests.
+ 2 files changed, 47 insertions(+), 5 deletions(-)
+
+Yossi Gottlieb in commit 8897c154:
+ CommandFilter API: Support Lua and RM_call() flows.
+ 2 files changed, 18 insertions(+), 7 deletions(-)
+
+Yossi Gottlieb in commit 6dd5bad4:
+ CommandFilter API: More cleanup.
+ 2 files changed, 10 insertions(+), 29 deletions(-)
+
+Yossi Gottlieb in commit 83026101:
+ Add command filter Module API tests.
+ 2 files changed, 28 insertions(+)
+
+Yossi Gottlieb in commit dc5edc7b:
+ Add command filtering argument handling API.
+ 3 files changed, 132 insertions(+), 13 deletions(-)
+
+Yossi Gottlieb in commit 5f29e2e2:
+ Initial command filter experiment.
+ 6 files changed, 161 insertions(+), 2 deletions(-)
+
+Oran Agra in commit e1839ab3:
+ diskless fork kept streaming RDB to a disconnected slave
+ 1 file changed, 10 insertions(+)
+
+Oran Agra in commit 3b207b89:
+ diskless replication - notify slave when rdb transfer failed
+ 1 file changed, 1 insertion(+)
+
+antirez in commit 7e350b09:
+ More sensible name for function: restartAOFAfterSYNC().
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+antirez in commit 91238a60:
+ Mostly aesthetic changes to restartAOF().
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+oranagra in commit ee2da67c:
+ bugfix to restartAOF, exit will never happen since retry will get negative.
+ 1 file changed, 5 insertions(+), 4 deletions(-)
+
+Oran Agra in commit 78022492:
+ Add log when server dies of SIGTERM during loading
+ 1 file changed, 1 insertion(+)
+
+Yossi Gottlieb in commit 232dca7f:
+ Add RedisModule_GetKeyNameFromIO().
+ 8 files changed, 30 insertions(+), 17 deletions(-)
+
+antirez in commit 7f98129a:
+ MANIFESTO: simplicity and lock-in.
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+antirez in commit 71265fe3:
+ MANIFESTO v2.
+ 1 file changed, 41 insertions(+), 6 deletions(-)
+
+yongman in commit 8115be6e:
+ Fix uint64_t hash value in active defrag
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Angus Pearson in commit 90e7b5a9:
+ Enlarge error buffer in redis-check-aof.c to remove compiler warning of output truncation through snprintf format string
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit 43151baf:
+ fix memory leak when rewrite config file
+ 1 file changed, 3 insertions(+), 4 deletions(-)
+
+唐权 in commit d3c17c9d:
+ Update ziplist.c
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+stan011 in commit 296bd097:
+ change the comments there may have a mis type
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Yossi Gottlieb in commit e08c9c15:
+ Preserve client->id for blocked clients.
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit c6b1252f:
+ aof: enhance AOF_FSYNC_EVERYSEC, more details in #5985
+ 2 files changed, 32 insertions(+), 3 deletions(-)
+
+David Carlier in commit ce54e299:
+ build fix
+ 1 file changed, 1 insertion(+)
+
+yongman in commit c9274498:
+ Fix memleak in bitfieldCommand
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+James Rouzier in commit 635d8d83:
+ Fix start and end key initialize
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit 7c23e534:
+ Merge pull request #6047 from abhaynahar/removed-obsolete-warning-5.0
+abhay in commit 9ea8ec42:
+ removed obsolete warning as per - https://github.com/antirez/redis/issues/5291
+ 1 file changed, 1 insertion(+), 7 deletions(-)
+
+antirez in commit 1b7407fa:
+ Aesthetic change to #5962 to conform to Redis style.
+ 1 file changed, 1 insertion(+), 3 deletions(-)
+
+Oran Agra in commit 3bbf9747:
+ slave corrupts replication stream when module blocked client uses large reply (or POSTPONED_ARRAY)
+ 3 files changed, 15 insertions(+), 6 deletions(-)
+
+================================================================================
+Redis 5.0.4 Released Mon Mar 18 17:12:53 CET 2019
+================================================================================
+
+Upgrade urgency HIGH: This release fixes several Redis stability issues.
+
+Dear Redis users, this release includes a number of fixes for bugs that may
+result in Redis crashing in special conditions (not normal usage, but specific
+artificial conditions), fixes to certain Redis behaviors especially around
+Redis streams, and finally a set of new APIs for Redis Modules.
+
+Specifically:
+
+* Hyperloglog different coding errors leading to potential crashes were fixed.
+* A replication bug leading to a potential crash in case of plain misuse of handshake commands was fixed.
+* XCLAIM command incrementing of number of deliveries was fixed.
+* LFU field management in objects was improved.
+* A potential overflow in the redis-check-aof was fixed.
+* A memory leak in case of API misuse was fixed.
+* ZPOP* behavior when count is 0 is fixed.
+* A few redis-cli --cluster bugs were fixed, plus a few improvements.
+* Many other smaller bugs.
+
+We suggest to upgrade Redis, especially in case your instance is facing
+untrusted users (for instance Cloud providers) because several of these
+bugs could result in unwanted crashes.
+
+This is the list of commits:
+
+antirez in commit 84bdd440:
+ HyperLogLog: fix comment in hllCount().
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit ef1833b3:
+ HyperLogLog: handle wrong offset in the base case.
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+antirez in commit 623afd5e:
+ HyperLogLog: speedup fuzz test.
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+antirez in commit 12b5ff10:
+ HyperLogLog: enlarge reghisto variable for safety.
+ 1 file changed, 6 insertions(+), 1 deletion(-)
+
+antirez in commit 254d897e:
+ HyperLogLog: dense/sparse repr parsing fuzz test.
+ 1 file changed, 29 insertions(+)
+
+John Sully in commit 7f79849c:
+ Fix hyperloglog corruption
+ 1 file changed, 6 insertions(+)
+
+Brad Solomon in commit 3ef2c831:
+ Provide an uninstall target in Makefile
+ 1 file changed, 3 insertions(+)
+
+antirez in commit 57aea463:
+ redis-check-aof: fix potential overflow.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit ba5145b8:
+ Fix objectSetLRUOrLFU() when LFU underflows.
+ 1 file changed, 11 insertions(+), 7 deletions(-)
+
+antirez in commit 76c59f0e:
+ Fix ZPOP return type when COUNT=0. Related to #5799.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 1c636714:
+ Improve comments after merging #5834.
+ 2 files changed, 14 insertions(+), 8 deletions(-)
+
+Guy Benoish in commit 6a3fca4c:
+ Trim SDS free space of retained module strings
+ 4 files changed, 28 insertions(+), 5 deletions(-)
+
+Guy Benoish in commit 9ec144ea:
+ Fix mismatching keyspace notification classes
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+Guy Benoish in commit d04b5211:
+ Fix zlexrangespec mem-leak in genericZrangebylexCommand
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+Guy Benoish in commit 516f1c77:
+ Use memtoll() in 'CONFIG SET client-output-buffer-limit'
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Guy Benoish in commit 8db67a55:
+ Increase string2ld's buffer size (and fix HINCRBYFLOAT)
+ 2 files changed, 5 insertions(+), 1 deletion(-)
+
+Guy Benoish in commit db3d626b:
+ Check server.verbosity in RM_LogRaw
+ 1 file changed, 2 insertions(+)
+
+Guy Benoish in commit 71439a07:
+ ZPOP should return an empty array if COUNT=0
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+antirez in commit c8a26834:
+ Modules shared API: export new core APIs.
+ 2 files changed, 6 insertions(+)
+
+antirez in commit a13ba750:
+ Modules shared API: also unregister the module as user.
+ 1 file changed, 23 insertions(+)
+
+antirez in commit 500e5117:
+ Modules shared API: prevent unloading of used modules.
+ 1 file changed, 10 insertions(+), 4 deletions(-)
+
+antirez in commit 7854daa1:
+ Modules shared API: unregister APIs function.
+ 1 file changed, 25 insertions(+)
+
+antirez in commit d38d82af:
+ Modules shared API: initial core functions.
+ 2 files changed, 89 insertions(+), 1 deletion(-)
+
+antirez in commit 4d747bb8:
+ Revert shared APIs to modify the design.
+ 3 files changed, 120 deletions(-)
+
+MeirShpilraien in commit 8824b509:
+ added module ability to register api to be used by other modules
+ 3 files changed, 120 insertions(+)
+
+zhaozhao.zz in commit 000b055b:
+ Streams: checkType before XGROUP CREATE
+ 1 file changed, 7 insertions(+), 5 deletions(-)
+
+antirez in commit 9b2a0d54:
+ Fix BZPOP arity, backport from fix in cd2743c.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+chendianqiang in commit 134b2582:
+ optimize cluster failover
+ 1 file changed, 1 insertion(+)
+
+Steve Webster in commit 1293e2a5:
+ Only increment delivery count if JUSTID option is omitted
+ 2 files changed, 18 insertions(+), 3 deletions(-)
+
+Steve Webster in commit 3cc4f469:
+ Increment delivery counter on XCLAIM unless RETRYCOUNT specified
+ 2 files changed, 35 insertions(+), 2 deletions(-)
+
+antirez in commit f4edd2b9:
+ Merge branch '5.0' of github.com:/antirez/redis into 5.0
+swilly22 in commit cedcc54e:
+ document additional flag of RM_GetContextFlags
+ 1 file changed, 3 insertions(+)
+
+swilly22 in commit 26e98da2:
+ Extend REDISMODULE_CTX_FLAGS to indicate if command was sent by master
+ 2 files changed, 6 insertions(+)
+
+Salvatore Sanfilippo in commit 0e910939:
+ Merge pull request #5879 from meierfra-ergon/redis-cli-assume-yes
+antirez in commit 67452e91:
+ Make comment in #5911 stay inside 80 cols.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+John Sully in commit 30f666ef:
+ Replicas aren't allowed to run the replicaof command
+ 1 file changed, 8 insertions(+)
+
+Frank Meier in commit bc6c1c40:
+ extend use of cluster-yes option to other confimation questions
+ 1 file changed, 9 insertions(+)
+
+antirez in commit 76419d8d:
+ Merge branch '5.0' of github.com:/antirez/redis into 5.0
+Oran Agra in commit 72ba6069:
+ redis-cli add support for --memkeys, fix --bigkeys for module types
+ 1 file changed, 132 insertions(+), 81 deletions(-)
+
+chendianqiang in commit 2ca21753:
+ fix replicationid will not change for server.masterhost==NULL in cluster mode when restart slave
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit bd7ddd79:
+ Merge pull request #5870 from fengweiyuan/5.0
+varianfeng in commit d13bc143:
+ fix corrupt_rdb.c bug.Let the name of input rdb file name be valid.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+artix in commit 44c5bce0:
+ Cluster Manager: fix replica assigment anti-affinity (create)
+ 1 file changed, 6 insertions(+)
+
+artix in commit f066e526:
+ Cluster Manager: remove unused code elements
+ 1 file changed, 8 insertions(+), 13 deletions(-)
+
+Zhicheng Wei in commit 23214966:
+ fix clusterManagerGetAntiAffinityScore double free otypes
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit 80bccd71:
+ Remove debugging printf from replication.tcl test.
+ 1 file changed, 1 deletion(-)
+
+================================================================================
+Redis 5.0.3 Released Tue Dec 11 18:17:26 CET 2018
+================================================================================
+
+Upgrade urgency HIGH: Redis 5 is consolidating, upgrading is a good idea.
+ However there is nothing very critical here, but certain
+ issues resolved could lead to very rare crashes.
+
+Welcome to Redis 5.0.3, several interesting bug fixes here:
+
+* Redis no longer panics when you send data to a replica-mode connection that
+ is in MONITOR or SYNC mode.
+
+* Fixes to certain sorted set edge cases. You are unlikely to ever notice those
+ issues, but now it is more correct.
+
+* Certain BSD variants now are better supported: build & register logging
+ on crash.
+
+* The networking core now recovers if an IPv6 address is listed in bind but
+ is actually not able to work because there is no such protocol in the
+ system.
+
+* redis-cli cluster mode improved in many ways. Especially the fix subcommand
+ work was enhanced to cover other edge cases that were still not covered
+ after the work done for Redis 5.
+
+* MEMORY USAGE is now more accurate.
+
+* DEBUG DIGEST-VALUE added in case you want to make sure a given set of keys
+ (and not the whole DB) are excatly the same between two instances.
+
+* Fix a potential crash in the networking code related to recent changes
+ to the way the reply is consumed.
+
+* Reject EXEC containing write commands against an instance that changed role
+ from master to replica during our transaction.
+
+* Fix a crash in KEYS and other commands using pattern matching, in an edge
+ case where the pattern contains a zero byte.
+
+* Fix eviction during AOF loading due to maxmemory triggered by commands
+ executed in loading state.
+
+The following is the list of commmits if you want to check credits or dig
+further in the details.
+
+commit 2c6ee0f9b3d9ca48c6da8bd18796186784216bff
+Author: antirez <antirez@gmail.com>
+Date: Wed Dec 12 11:37:15 2018 +0100
+
+ freeMemoryIfNeeded() small refactoring.
+
+ Related to issue #5686 and PR #5689.
+
+commit 107e93e75acfd5def0252efb6870751940816395
+Author: zhaozhao.zz <zhaozhao.zz@alibaba-inc.com>
+Date: Wed Dec 12 00:25:24 2018 +0800
+
+ evict: don't care about mem if loading
+
+ When loading data, we call processEventsWhileBlocked
+ to process events and execute commands.
+ But if we are loading AOF it's dangerous, because
+ processCommand would call freeMemoryIfNeeded to evict,
+ and that will break data consistency, see issue #5686.
+
+antirez in commit ee93dc0b:
+ Crashing is too much in addReplyErrorLength().
+ 1 file changed, 6 deletions(-)
+
+hdmg in commit c55254a5:
+ fix comments fault discription
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+lsytj0413 in commit dfd25013:
+ fix a typo: craeted -> created
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 392a2566:
+ stringmatchlen() fuzz test added.
+ 3 files changed, 22 insertions(+)
+
+antirez in commit 7602f695:
+ Fix stringmatchlen() read past buffer bug.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit c4f3585e:
+ multi: ignore multiState's cmd_flags when loading AOF
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit d037e987:
+ Reject EXEC containing write commands against RO replica.
+ 2 files changed, 20 insertions(+)
+
+artix in commit e00ab324:
+ Cluster Manager: - Multiple owners checking in 'fix'/'check' commands is
+ now optional (using --cluster-search-multiple-owners). - Updated help.
+ 1 file changed, 14 insertions(+), 5 deletions(-)
+
+artix in commit 94f64de3:
+ Cluster Manager: FixOpenSlot now correctly updates in-memory cluster
+ configuration. Improved output messages.
+ 1 file changed, 17 insertions(+), 5 deletions(-)
+
+artix in commit 752d636f:
+ Cluster Manager: 'fix' command now handles open slots with migrating state
+ in one node and importing state in multiple nodes.
+ 1 file changed, 74 insertions(+), 6 deletions(-)
+
+artix in commit 552091f9:
+ Cluster Manager: setting new slot owner is now handled atomically in
+ 'fix' command.
+ 1 file changed, 72 insertions(+), 31 deletions(-)
+
+artix in commit 2280f4f7:
+ Cluster Manager: code cleanup.
+ 1 file changed, 41 insertions(+), 87 deletions(-)
+
+artix in commit e084b8cc:
+ Cluster Manager: check/fix commands now handle multiple owners even
+ if all slots are covered and not open.
+ 1 file changed, 129 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit fa726e2a:
+ remove useless tryObjectEncoding in debug assert
+ 1 file changed, 1 deletion(-)
+
+Oran Agra in commit 40244b10:
+ fix #5580, display fragmentation and rss overhead bytes as signed
+ 2 files changed, 6 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit beab3151:
+ networking: current_client should not be NULL when trim qb_pos
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 07ccb642:
+ Remove no longer relevant comment in processCommand().
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+antirez in commit 60fdaf07:
+ DEBUG DIGEST-VALUE implemented.
+ 1 file changed, 17 insertions(+), 3 deletions(-)
+
+antirez in commit 48b31b0d:
+ DEBUG DIGEST refactoring: extract function to digest a value.
+ 1 file changed, 142 insertions(+), 131 deletions(-)
+
+yura in commit ef3ff402:
+ redis-cli reshard/rebalance: ability to force replacement on existing keys
+ 1 file changed, 6 insertions(+), 5 deletions(-)
+
+Thomas Orozco in commit ee223fb8:
+ cli: pass auth through REDISCLI_AUTH
+ 1 file changed, 14 insertions(+)
+
+yongman in commit 41295e55:
+ Fix cluster call reply format readable
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit 0ed3970f:
+ fix small test suite race conditions
+ 3 files changed, 11 insertions(+)
+
+zhaozhao.zz in commit 605dddbb:
+ MEMORY command: make USAGE more accurate
+ 1 file changed, 7 insertions(+), 6 deletions(-)
+
+yongman in commit 1f43bf29:
+ Fix choose a random master node for slot assignment
+ 1 file changed, 29 insertions(+), 5 deletions(-)
+
+Weiliang Li in commit 69f0c678:
+ fix comment typo in util.c
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Chris Lamb in commit bc53a3ab:
+ Clarify the "Creating Server TCP listening socket" error.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Chris Lamb in commit fefe5460:
+ Don't treat unsupported protocols as fatal errors
+ 1 file changed, 4 insertions(+)
+
+David Carlier in commit a8862972:
+ OpenBSD support.
+ 3 files changed, 74 insertions(+), 1 deletion(-)
+
+David Carlier in commit 5e86daf9:
+ Backtrace/register dump on BSD.
+ 3 files changed, 97 insertions(+), 3 deletions(-)
+
+Guy Benoish in commit 7c8cf5ac:
+ Don't call sdscmp() with shared.maxstring or shared.minstring
+ 2 files changed, 23 insertions(+), 9 deletions(-)
+
+Qu Chen in commit 39e9eda3:
+ Add unit test for stream XCLAIM command.
+ 1 file changed, 48 insertions(+)
+
+antirez in commit 62485232:
+ Abort instead of crashing when loading bad stream master key.
+ 1 file changed, 3 insertions(+)
+
+Madelyn Olson in commit a5487309:
+ Fixed a serverPanic when sending an invalid command to a monitor client
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0.2 Released Thu Nov 22 11:22:37 CET 2018
+================================================================================
+
+Upgrade urgency: CRITICAL if you use streams and consumer groups.
+ HIGH if you use redis-cli with Redis Cluster.
+ LOW otherwise.
+
+Welcome to Redis 5.0.2. This release fixes two issues with Streams consumer
+groups, where items could be returned duplicated by XREADGROUP when accessing
+the history, and another bug where XREADGROUP can report some history even
+if the comsumer pending list is empty. Both problems were addressed and unit
+tests to avoid regressions implemented. Moreover this release fixes some
+issue with redis-cli when in cluster mode. Finally some FreeBSD and DragonFly
+build problems are now resolved. The list of the commits is below.
+
+Enjoy,
+Salvatore
+
+David Carlier in commit e8b4291a:
+ DragonFlyBSD little build fix
+ 2 files changed, 6 insertions(+), 1 deletion(-)
+
+yongman in commit 8fcfd374:
+ skip slave nodes when sending cluster setslot command
+ 1 file changed, 1 insertion(+)
+
+yongman in commit d7089ddd:
+ Fix pointer access and memory leak in redis-cli.
+ 1 file changed, 6 insertions(+), 3 deletions(-)
+
+antirez in commit 17b4cd83:
+ Test: regression test for #5570.
+ 1 file changed, 15 insertions(+)
+
+antirez in commit 45123169:
+ Stream: fix XREADGROUP history reading of deleted messages.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+David Carlier in commit 5ad588f0:
+ only FreeBSD change/little warning addressing
+ 2 files changed, 7 insertions(+), 4 deletions(-)
+
+David Carlier in commit 11801e1a:
+ tweak form feedback
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+David Carlier in commit c1f13575:
+ allow flavors
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+David Carlier in commit 275a2d49:
+ Fix clang build.
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+antirez in commit 44ad5141:
+ Test: regression test for #5577.
+ 1 file changed, 24 insertions(+)
+
+antirez in commit c7951f43:
+ Streams: fix XREADGROUP history reading when CG last_id is low.
+ 1 file changed, 12 insertions(+), 9 deletions(-)
+
+antirez in commit a69bc5be:
+ t_stream.c comment resized to 80 cols.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit 5314099d:
+ Redis 5 changelog: don't expect Lua replies to be ordered.
+ 1 file changed, 14 insertions(+), 5 deletions(-)
+
+================================================================================
+Redis 5.0.1 Released Wed Nov 07 13:09:30 CET 2018
+================================================================================
+
+Upgrade urgency: URGENT if you use Redis Streams. MODERATE otherwise.
+
+Hi all, this is the first patch level release of Redis 5. It contains
+both fixes and improvements. Here there is a list of the major ones, however
+read the commit messages at the end of the changelog if you want to know
+more about the smaller things. Let's start with the new features:
+
+* Sentinel now supports authentication! Check the Sentinel official doc
+ for more info.
+
+* Redis-cli cluster "fix" is now able to fix a big number of clusters put
+ in a bad condition. Previously many corner cases were not covered.
+
+Now the critical fixes:
+
+1. Fix RESTORE mismatch reply when certain keys already expired.
+2. Fix an XCLAIM non trivial issue: sometimes the command returned a wrong
+ entry or desynchronized the protocol.
+
+And now the other fixes:
+
+3. Stack trace generation on the Raspberry PI (and 32bit ARM) fixed.
+4. Don't evict expired keys when the KEYS command is called, in order to
+ avoid a mass deletion event. However expired keys are not displayed
+ by KEYS as usually.
+5. Improvements in the computation of the memory used, when estimating
+ the AOF buffers.
+6. XRANGE COUNT of 0 fixed.
+7. "key misses" stats accounting fixed. Many cache misses were not counted.
+8. When in MULTI state, return OOM while accumulating commands and there
+ is no longer memory available.
+9. Fix build on FreeBSD and possibly others.
+10. Fix a crash in Redis modules, thread safe context reply accumulation.
+11. Fix a race condition when producing the RDB file for full SYNC.
+12. Disable protected mode in Sentinel.
+13. More commands now have the HELP subcommand.
+14. Fixed an issue about adaptive server HZ timer.
+15. Fix cluster-replica-no-failover option name.
+
+Finally, this is the list of commits. Enjoy Redis 5.0.1!
+
+antirez in commit c801283f:
+ Fix cluster-replica-no-failover option name.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 4c4f50e1:
+ MEMORY command: make strcasecmp() conditional like the following.
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+Itamar Haber in commit a7b46e0e:
+ Uppercases subcommands in MEMORY HELP
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+Itamar Haber in commit 80e129d9:
+ Standardizes `MEMORY HELP` subcommand
+ 1 file changed, 13 insertions(+), 14 deletions(-)
+
+valentino in commit 88805cbb:
+ fix short period of server.hz being uninitialized
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Itamar Haber in commit 6b402733:
+ Adds HELP to LATENCY
+ 1 file changed, 14 insertions(+), 2 deletions(-)
+
+yongman in commit 1c637de9:
+ fix malloc in clusterManagerComputeReshardTable
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+artix in commit 90b52fde:
+ Cluster Manager: removed unused var.
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+artix in commit 89cbb5df:
+ Cluster Manager: further improvements to "fix": - clusterManagerFixOpenSlot: ensure that the slot is unassigned before ADDSLOTS - clusterManagerFixSlotsCoverage: after cold migration, the slot configuration is now updated on all the nodes.
+ 1 file changed, 49 insertions(+), 10 deletions(-)
+
+artix in commit 175515c9:
+ Cluster Manager: fixed string parsing issue in clusterManagerGetConfigSignature
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+artix in commit 3997dd6e:
+ Cluster Manager: better fix subcommand.
+ 1 file changed, 78 insertions(+), 20 deletions(-)
+
+artix in commit bd80291c:
+ Cluster Manager: fixed typos in comments.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+artix in commit 4369cbce:
+ Cluster Manager: fixed 'DELSLOT' subcommand typo.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 1ed821e2:
+ Fix XCLAIM missing entry bug.
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+michael-grunder in commit b49bcd01:
+ Use typedef'd mstime_t instead of time_t
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 09d1849e:
+ Improve streamReplyWithRange() top comment.
+ 1 file changed, 9 insertions(+), 5 deletions(-)
+
+antirez in commit bdf6306f:
+ Add support for Sentinel authentication.
+ 1 file changed, 17 insertions(+), 3 deletions(-)
+
+antirez in commit 50222af5:
+ Disable protected mode in Sentinel mode.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit 643ee6e3:
+ When replica kills a pending RDB save during SYNC, log it.
+ 1 file changed, 6 insertions(+)
+
+Andrey Bugaevskiy in commit 8b609c99:
+ Move child termination to readSyncBulkPayload
+ 1 file changed, 6 insertions(+), 7 deletions(-)
+
+Andrey Bugaevskiy in commit 27102605:
+ Prevent RDB autosave from overwriting full resync results
+ 1 file changed, 7 insertions(+)
+
+antirez in commit a677923d:
+ asyncCloseClientOnOutputBufferLimitReached(): don't free fake clients.
+ 1 file changed, 1 insertion(+)
+
+David Carlier in commit 427e440a:
+ needs it for the global
+ 1 file changed, 1 insertion(+)
+
+David Carlier in commit 28f9ca4e:
+ Fix non Linux build.
+ 3 files changed, 20 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit 4bf9efe2:
+ MULTI: OOM err if cannot free enough memory in MULTI/EXEC context
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+antirez in commit 4fbd7a39:
+ Add command fingerprint comment for XSETID.
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+Itamar Haber in commit 2480db53:
+ Plugs a potential underflow
+ 1 file changed, 1 insertion(+)
+
+Itamar Haber in commit e5e4d2ef:
+ Corrects inline documentation of syntax
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit 713800d2:
+ if we read a expired key, misses++
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+antirez in commit e79ee263:
+ Fix XRANGE COUNT option for value of 0.
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+antirez in commit 505cc70f:
+ Fix typo in streamReplyWithRange() top comment.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Damien Tournoud in commit 3c36561d:
+ Overhead is the allocated size of the AOF buffer, not its length
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 3761582f:
+ Simplify part of the #5470 patch.
+ 1 file changed, 11 insertions(+), 12 deletions(-)
+
+zhaozhao.zz in commit edc47a3a:
+ do not delete expired keys in KEYS command
+ 1 file changed, 34 insertions(+), 27 deletions(-)
+
+antirez in commit 9872af6d:
+ Use guide comments to make changes in #5462 more obvious.
+ 1 file changed, 6 insertions(+)
+
+youjiali1995 in commit 3f399c3b:
+ migrate: fix mismatch of RESTORE reply when some keys have expired.
+ 1 file changed, 8 insertions(+), 6 deletions(-)
+
+hujie in commit eaaff621:
+ fix typo in config.c
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+hujiecs in commit 43ebb7ee:
+ several typos fixed, optimize MSETNX to avoid unnecessary loop
+ 4 files changed, 4 insertions(+), 4 deletions(-)
+
+antirez in commit de8fdaac:
+ Remove useless complexity from MSET implementation.
+ 1 file changed, 5 insertions(+), 7 deletions(-)
+
+antirez in commit dc8f1112:
+ Fix again stack generation on the Raspberry Pi.
+ 1 file changed, 4 insertions(+)
+
+antirez in commit 83a6e81d:
+ Get rid of the word slave in the release note of Redis 5.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+================================================================================
+Redis 5.0.0 Released Wed Oct 17 13:28:26 CEST 2018
+================================================================================
+
+Upgrade urgency CRITICAL: Several fixes to streams AOF and replication.
+
+Hi all and welcome to the first stable release of Redis 5! \o/
+
+To start a quick recap of what's new in Redis 5:
+
+1. The new Stream data type. https://redis.io/topics/streams-intro
+2. New Redis modules APIs: Timers, Cluster and Dictionary APIs.
+3. RDB now store LFU and LRU information.
+4. The cluster manager was ported from Ruby (redis-trib.rb) to C code
+ inside redis-cli. Check `redis-cli --cluster help` for more info.
+5. New sorted set commands: ZPOPMIN/MAX and blocking variants.
+6. Active defragmentation version 2.
+7. Improvemenets in HyperLogLog implementations.
+8. Better memory reporting capabilities.
+9. Many commands with sub-commands now have an HELP subcommand.
+10. Better performances when clients connect and disconnect often.
+11. Many bug fixes and other random improvements.
+12. Jemalloc was upgraded to version 5.1
+13. CLIENT UNBLOCK and CLIENT ID.
+14. The LOLWUT command was added. http://antirez.com/news/123
+15. We no longer use the "slave" word if not for API backward compatibility.
+16. Differnet optimizations in the networking layer.
+17. Lua improvements:
+ - Better propagation of Lua scripts to replicas / AOF.
+ - Lua scripts can now timeout and get in -BUSY state in the replica as well.
+18. Dynamic HZ to balance idle CPU usage with responsiveness.
+19. The Redis core was refactored and improved in many ways.
+
+However the list above really does not do justice to the changes of Redis 5
+since the core was improved in many ways during the development of the new
+version. However certain changes were back ported into Redis 4 once they were
+sensed as safe, because many improvements were hard to distinguish from fixes.
+
+The most important user facing improvement is without doubts the introduction
+of the new general purpose data type after years: the streams.
+
+Note that we worked to improve and fix streams till a few hours ago, so while
+we are not aware of critical bugs in this release, surely there is to handle it
+with some care for the first weeks. Bug reporting will be highly appreciated and
+we are ready to work immediately to release 5.0.1 once there is enough important
+stuff to justify a new release (probably soon).
+
+People not using the streams can have probably a better production-ready
+experience with Redis 5, also because many internals are shared with Redis 4
+so the jump is not as big as it was between 3.2 and 4 in terms of how things
+internally work.
+
+Well, many thanks to the Redis community and the developers that made
+this release possible, contributing bug reports, patches, new features, working
+on the clients, sometimes debugging problems for days. Also thank to everybody
+that adopted Redis for their use cases making things work for users worldwide.
+
+The list of commits in this release follows.
+
+Cheers,
+Salvatore
+
+antirez in commit bcc0916d:
+ Fix conditional in XGROUP.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 1b2f23f3:
+ Update help.h for redis-cli.
+ 1 file changed, 57 insertions(+), 7 deletions(-)
+
+antirez in commit de0ae56c:
+ Tests for XGROUP CREATE MKSTREAM.
+ 1 file changed, 11 insertions(+)
+
+antirez in commit 56c3dfa1:
+ Fix XGROUP CREATE MKSTREAM handling of .
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+antirez in commit 2687f228:
+ Process MKSTREAM option of XGROUP CREATE at a later time.
+ 1 file changed, 28 insertions(+), 17 deletions(-)
+
+zhaozhao.zz in commit cfbaf8f1:
+ Scripting & Streams: some commands need right flags
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit 4e4099b9:
+ XGROUP CREATE: MKSTREAM option for automatic stream creation.
+ 1 file changed, 29 insertions(+), 5 deletions(-)
+
+zhaozhao.zz in commit 6dd4d864:
+ Streams: Tests modified XSTREAM -> XSETID
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+zhaozhao.zz in commit 3aff0e8c:
+ Streams: rewrite empty streams with certain lastid
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 880b563e:
+ Tests modified to use XADD MAXLEN 0 + XSETID.
+ 1 file changed, 12 insertions(+), 26 deletions(-)
+
+antirez in commit 83c87835:
+ Streams: rewrite empty streams with XADD MAXLEN 0. Use XSETID.
+ 1 file changed, 18 insertions(+), 12 deletions(-)
+
+antirez in commit fd22e3ac:
+ XSETID: accept IDs based on last entry.
+ 1 file changed, 18 insertions(+), 5 deletions(-)
+
+antirez in commit dfab3cba:
+ Streams: XSTREAM SETID -> XSETID.
+ 3 files changed, 17 insertions(+), 67 deletions(-)
+
+zhaozhao.zz in commit a3fb28ed:
+ Streams: rewrite id in XSTREAM CREATE *
+ 1 file changed, 4 insertions(+)
+
+zhaozhao.zz in commit f4b4db13:
+ Streams: add tests for aof rewrite
+ 1 file changed, 23 insertions(+)
+
+zhaozhao.zz in commit d22f1ef0:
+ Stream & AOF: rewrite stream in correct way
+ 1 file changed, 32 insertions(+), 16 deletions(-)
+
+zhaozhao.zz in commit 6455274d:
+ Streams: add tests for XSTREAM command
+ 1 file changed, 39 insertions(+)
+
+zhaozhao.zz in commit 0edbe953:
+ Streams: add a new command XTREAM
+ 3 files changed, 67 insertions(+)
+
+Hamid Alaei in commit 9714bba2:
+ fix timer context selected database
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit eb53f15a:
+ Make comment about nack->consumer test for minidle more obvious.
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+antirez in commit a77f836e:
+ Streams: use propagate_last_id itself as streamPropagateGroupID trigger.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 0f0610eb:
+ Streams: better naming: lastid_updated -> propagate_last_id.
+ 1 file changed, 6 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit a745e423:
+ Streams: panic if streamID invalid after check, should not be possible.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit 9974be13:
+ Streams: propagate lastid in XCLAIM when it has effect
+ 1 file changed, 13 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit 69a628d0:
+ Streams: XCLAIM ignore minidle if NACK is created by FORCE
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+zhaozhao.zz in commit a04b43c7:
+ Streams: bugfix XCLAIM should propagate group name not consumer name
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Sergey Chupov in commit 8977a90c:
+ fixed typos in readme
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 3a745674:
+ redis.conf typo fixed: ingore -> ignore.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 22770d76:
+ Rax: radix tree updated to latest version from antirez/rax.
+ 2 files changed, 233 insertions(+), 68 deletions(-)
+
+antirez in commit fbac534f:
+ Test: avoid time related false positive in RESTORE test.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 49872337:
+ LOLWUT: capitalize Nees.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 80c471f5:
+ Test: cgroup propagation test also for NOACK variant.
+ 1 file changed, 39 insertions(+), 29 deletions(-)
+
+antirez in commit 8defa5da:
+ Test: consumer group last ID slave propagation test.
+ 1 file changed, 39 insertions(+)
+
+zhaozhao.zz in commit e1e3eaca:
+ Avoid recreate write handler for protected client.
+ 1 file changed, 4 insertions(+)
+
+antirez in commit b501fd5d:
+ Fix propagation of consumer groups last ID.
+ 3 files changed, 56 insertions(+), 9 deletions(-)
+
+
+================================================================================
+Redis 5.0-rc6 Released Wed Oct 10 11:03:54 CEST 2018
+================================================================================
+
+Upgrade urgency HIGH: Many bugs fixed especially in the context of streams.
+
+This is probably the last release candidate of Redis 5. The Redis 5 GA version
+will be released 17th of October. The main highlights of this release are:
+
+* Critical AOF bug, as old as AOF itself: if an open MULTI/EXEC block is at
+ the end of the AOF file, Redis would still read the half-transaction when
+ reloading back the AOF.
+* The slave name was removed from logs and documentation, now replica is used
+ instead.
+* LOLWUT command added.
+* New modules APIs: Disable Redis Cluster redirection.
+* New modules APIs: Sorted dictionaries data type.
+* Modules APIs fixes: timer / cluster messages callback now can call RM_Call().
+* Fix for #5024 - commandstats for multi-exec were logged as EXEC.
+* A number of optimizations and fixes for the stream data type.
+* Many other stability improvements.
+
+This is the list of comments and contributors:
+
+antirez in commit 9a6fa7d0:
+ changelog.tcl: get optional argument for number of commits.
+ 1 file changed, 8 insertions(+), 3 deletions(-)
+
+antirez in commit 101e419f:
+ Free protected clients asynchronously.
+ 1 file changed, 7 insertions(+)
+
+antirez in commit 726debb8:
+ Actually use the protectClient() API where needed.
+ 2 files changed, 8 insertions(+), 9 deletions(-)
+
+antirez in commit 0b87f78a:
+ Introduce protectClient() + some refactoring.
+ 2 files changed, 60 insertions(+), 18 deletions(-)
+
+zhaozhao.zz in commit 6aa8ac70:
+ debug: avoid free client unexpectedly when reload & loadaof
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+antirez in commit 48040b02:
+ aof.c: improve indentation and change warning message.
+ 1 file changed, 11 insertions(+), 4 deletions(-)
+
+zhaozhao.zz in commit 7cc20569:
+ AOF: discard if we lost EXEC when loading aof
+ 2 files changed, 14 insertions(+), 3 deletions(-)
+
+antirez in commit 2007d30c:
+ Refactoring of XADD / XTRIM MAXLEN rewriting.
+ 1 file changed, 15 insertions(+), 22 deletions(-)
+
+zhaozhao.zz in commit 6a298110:
+ Streams: add test cases for XADD/XTRIM maxlen
+ 1 file changed, 46 insertions(+)
+
+zhaozhao.zz in commit 041161b7:
+ Streams: propagate specified MAXLEN instead of approximated
+ 1 file changed, 35 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit f04d799b:
+ Streams: reset approx_maxlen in every maxlen loop
+ 1 file changed, 2 insertions(+)
+
+zhaozhao.zz in commit affd9365:
+ Streams: XTRIM will return an error if MAXLEN with a count < 0
+ 1 file changed, 6 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit 4c405ad0:
+ Streams: propagate original MAXLEN argument in XADD context
+ 1 file changed, 3 insertions(+), 12 deletions(-)
+
+antirez in commit 5c6d4b4a:
+ Fix typo in replicationCron() comment.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit a67a8dbf:
+ Fix typo in design comment of bio.c.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit c4ab5a05:
+ xclaimCommand(): fix comment typos.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit dc0b628a:
+ streamAppendItem(): Update the radix tree pointer only if changed.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit 4566fbc7:
+ Listpack: optionally force reallocation on inserts.
+ 1 file changed, 20 insertions(+)
+
+antirez in commit 5eca170c:
+ Fix printf type mismatch in genRedisInfoString().
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 260b53a2:
+ streamIteratorRemoveEntry(): set back lp only if pointer changed.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit 5d12f9d9:
+ Streams: update listpack with new pointer in XDEL
+ 1 file changed, 3 insertions(+)
+
+zhaozhao.zz in commit 6b7ad838:
+ bugfix: replace lastcmd with cmd when rewrite BRPOPLPUSH as RPOPLPUSH
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit 3454a043:
+ script cache memory in INFO and MEMORY includes both script code and overheads
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+Oran Agra in commit d6aeca86:
+ fix #5024 - commandstats for multi-exec were logged as EXEC.
+ 2 files changed, 63 insertions(+), 2 deletions(-)
+
+antirez in commit a996b2a2:
+ Fix XINFO comment for consistency.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Bruce Merry in commit 1a8447b6:
+ Fix invalid use of sdsZmallocSize on an embedded string
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Bruce Merry in commit 8dde46ad:
+ Fix incorrect memory usage accounting in zrealloc
+ 3 files changed, 24 insertions(+), 2 deletions(-)
+
+Hamid Alaei in commit b362a1b7:
+ fix dict get on not found
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 55e9df8a:
+ Try to avoid issues with GCC pragmas and older compilers.
+ 1 file changed, 7 insertions(+), 4 deletions(-)
+
+antirez in commit b0d22702:
+ Modules: hellodict example WIP #3: KEYRANGE.
+ 1 file changed, 40 insertions(+)
+
+antirez in commit af2f6682:
+ Modules: Modules: dictionary API WIP #13: Compare API exported.
+ 2 files changed, 6 insertions(+)
+
+antirez in commit f9a3e6ef:
+ Modules: Modules: dictionary API WIP #12: DictCompare API.
+ 1 file changed, 8 insertions(+)
+
+antirez in commit 01e0341a:
+ Modules: Modules: dictionary API WIP #11: DictCompareC API.
+ 1 file changed, 18 insertions(+)
+
+antirez in commit f9b3ce9a:
+ Modules: hellodict example WIP #1: GET command.
+ 1 file changed, 18 insertions(+)
+
+antirez in commit 36e66d86:
+ Modules: hellodict example WIP #1: SET command.
+ 1 file changed, 74 insertions(+)
+
+antirez in commit e33fdbe8:
+ Modules: remove useless defines in hellotimer.c
+ 2 files changed, 6 insertions(+), 4 deletions(-)
+
+antirez in commit 1c8b2248:
+ Modules: fix top comment of hellotimer.c
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Guy Korland in commit 7ded552d:
+ add missing argument to function doc
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Pavel Skuratovich in commit f92b3273:
+ Fix typo in comment
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 57b6c343:
+ Modules: dictionary API WIP #10: export API to modules.
+ 2 files changed, 60 insertions(+)
+
+antirez in commit 3f82e59c:
+ Modules: dictionary API WIP #9: iterator returning string object.
+ 1 file changed, 23 insertions(+), 6 deletions(-)
+
+antirez in commit 6a73aca3:
+ Modules: dictionary API WIP #8: Iterator next/prev.
+ 1 file changed, 42 insertions(+)
+
+antirez in commit ef8413db:
+ Modules: dictionary API WIP #7: don't store the context.
+ 1 file changed, 7 insertions(+), 8 deletions(-)
+
+antirez in commit 05579e38:
+ Modules: dictionary API WIP #6: implement automatic memory management.
+ 1 file changed, 21 insertions(+), 7 deletions(-)
+
+antirez in commit 11c53f8c:
+ Modules: dictionary API work in progress #5: rename API for consistency.
+ 1 file changed, 25 insertions(+), 25 deletions(-)
+
+antirez in commit 0bd7091b:
+ Modules: change RedisModuleString API to allow NULL context.
+ 1 file changed, 33 insertions(+), 12 deletions(-)
+
+antirez in commit 5fc16f17:
+ Modules: dictionary API work in progress #4: reseek API.
+ 1 file changed, 25 insertions(+), 6 deletions(-)
+
+antirez in commit 45b7f779:
+ Modules: dictionary API work in progress #3: Iterator creation.
+ 1 file changed, 41 insertions(+), 1 deletion(-)
+
+antirez in commit 8576b0ae:
+ Modules: dictionary API work in progress #2: Del API.
+ 1 file changed, 17 insertions(+), 2 deletions(-)
+
+antirez in commit 4b0fa7a7:
+ Modules: dictionary API work in progress #1.
+ 2 files changed, 95 insertions(+), 1 deletion(-)
+
+antirez in commit 28210760:
+ Module cluster flags: use RM_SetClusterFlags() in the example.
+ 2 files changed, 11 insertions(+)
+
+antirez in commit 18c5ab93:
+ Module cluster flags: add RM_SetClusterFlags() API.
+ 3 files changed, 33 insertions(+)
+
+antirez in commit 4ce6bff2:
+ Module cluster flags: add hooks for NO_FAILOVER flag.
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+antirez in commit 2ba52889:
+ Module cluster flags: add hooks for NO_REDIRECTION flag.
+ 3 files changed, 14 insertions(+), 4 deletions(-)
+
+antirez in commit 6a39ece6:
+ Module cluster flags: initial vars / defines added.
+ 5 files changed, 20 insertions(+)
+
+antirez in commit 0ff35370:
+ Modules: rename the reused static client to something more general.
+ 1 file changed, 10 insertions(+), 8 deletions(-)
+
+antirez in commit 2d11ee95:
+ Modules: associate a fake client to timer context callback.
+ 1 file changed, 2 insertions(+)
+
+antirez in commit 851b2ed3:
+ Modules: associate a fake client to cluster message context callback.
+ 1 file changed, 2 insertions(+)
+
+artix in commit 148e4911:
+ Cluster Manager: clusterManagerFixOpenSlot now counts node's keys in slot if node is neither migrating nor importing.
+ 1 file changed, 20 insertions(+), 1 deletion(-)
+
+Guy Korland in commit 8afca145:
+ No need to return "OK"
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Guy Korland in commit 9a278db2:
+ typo fix
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 26479036:
+ Revert "fix repeat argument issue and reduce unnessary loop times for redis-cli."
+ 1 file changed, 7 insertions(+), 12 deletions(-)
+
+Guy Korland in commit 27b7fb5a:
+ Fix few typos
+ 1 file changed, 10 insertions(+), 10 deletions(-)
+
+Guy Korland in commit 233aa2d3:
+ RedisModule_HashSet call must end with NULL
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit a8494072:
+ Sentinel: document how to undo a renamed command.
+ 1 file changed, 6 insertions(+), 1 deletion(-)
+
+antirez in commit 6c8a8f2e:
+ LOLWUT: split the command from version-specific implementations.
+ 3 files changed, 297 insertions(+), 241 deletions(-)
+
+antirez in commit 5c758406:
+ Slave removal: add a few forgotten aliases for CONFIG SET.
+ 1 file changed, 10 insertions(+)
+
+antirez in commit 2da823c4:
+ LOLWUT: add Redis version in the output.
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit bfcba420:
+ LOLWUT: Ness -> Nees.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit efed898a:
+ LOLWUT: Limit maximum CPU effort.
+ 1 file changed, 5 insertions(+)
+
+antirez in commit eb0fbd71:
+ LOLWUT: change padding conditional to a more direct one.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Slobodan Mišković in commit ed08feb7:
+ Fix spelling descrive -> describe
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 2ffb4413:
+ LOLWUT: fix crash when col < 2.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 55dae693:
+ LOLWUT: fix structure typo in comment.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 9b3098b9:
+ LOLWUT: Fix license copyright year.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 263dbadc:
+ LOLWUT: increase the translation factor.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit a622f6c0:
+ LOLWUT: change default size to fit a normal terminal better.
+ 1 file changed, 6 insertions(+), 6 deletions(-)
+
+antirez in commit 38b0d25a:
+ LOLWUT: wrap it into a proper command.
+ 4 files changed, 40 insertions(+), 15 deletions(-)
+
+antirez in commit 34ebd898:
+ LOLWUT: draw Schotter by Georg Nees.
+ 1 file changed, 47 insertions(+), 3 deletions(-)
+
+antirez in commit 46286e64:
+ LOLWUT: draw rotated squares using trivial trigonometry.
+ 1 file changed, 44 insertions(+)
+
+antirez in commit 2d4143fd:
+ LOLWUT: draw lines using Bresenham algorithm.
+ 1 file changed, 26 insertions(+), 2 deletions(-)
+
+antirez in commit 3546d9ce:
+ LOLWUT: Rendering of the virtual canvas to a string.
+ 1 file changed, 78 insertions(+), 7 deletions(-)
+
+antirez in commit b404a6ce:
+ LOLWUT: show the output verbatim in redis-cli.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit e30ba94f:
+ LOLWUT: canvas structure and BSD license on top.
+ 1 file changed, 46 insertions(+)
+
+antirez in commit 9c771145:
+ LOLWUT: Emit Braille unicode according to pixel pattern.
+ 1 file changed, 23 insertions(+)
+
+Jakub Vrana in commit 4a1d6c7d:
+ Slave removal: capitalize Replica
+ 2 files changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit 72e0368a:
+ Slave removal: remove slave from integration tests descriptions.
+ 8 files changed, 36 insertions(+), 36 deletions(-)
+
+antirez in commit c7841c2b:
+ Slave removal: remove slave from top-level tests descriptions.
+ 3 files changed, 12 insertions(+), 12 deletions(-)
+
+antirez in commit 1b9b19ba:
+ Slave removal: remove slave from object.c.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 7da266e6:
+ Slave removal: remove slave from the README.
+ 1 file changed, 7 insertions(+), 7 deletions(-)
+
+antirez in commit 93d803c9:
+ Slave removal: server.c logs fixed.
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit 89434032:
+ Slave removal: remove slave from sentinel.conf when possible.
+ 1 file changed, 18 insertions(+), 18 deletions(-)
+
+antirez in commit 7673d88d:
+ Slave removal: replace very few things in Sentinel.
+ 1 file changed, 12 insertions(+), 8 deletions(-)
+
+antirez in commit f1de29b3:
+ Slave removal: scripting.c logs and other stuff fixed.
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+antirez in commit 53fe558e:
+ Slave removal: replication.c logs fixed.
+ 1 file changed, 35 insertions(+), 35 deletions(-)
+
+antirez in commit c92b02dd:
+ Slave removal: networking.c logs fixed.
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit be76ed0c:
+ Slave removal: blocked.c logs fixed.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 3fd73151:
+ Slave removal: Make obvious in redis.conf what a replica is.
+ 1 file changed, 5 insertions(+)
+
+antirez in commit a22168e4:
+ Slave removal: slave mode -> replica mode text in redis-cli.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 0e222fbe:
+ Slave removal: fix typo of replicaof.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 34a5615e:
+ Slave removal: slave -> replica in redis.conf and output buffer option.
+ 3 files changed, 132 insertions(+), 129 deletions(-)
+
+antirez in commit 1d2fcf6f:
+ Slave removal: Convert cluster.c log messages and command names.
+ 1 file changed, 12 insertions(+), 11 deletions(-)
+
+antirez in commit 2546158d:
+ Slave removal: config.c converted + config rewriting hacks.
+ 1 file changed, 117 insertions(+), 38 deletions(-)
+
+antirez in commit c0952c0d:
+ Slave removal: redis-cli --slave -> --replica.
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit 1f37f1dd:
+ Slave removal: SLAVEOF -> REPLICAOF. SLAVEOF is now an alias.
+ 3 files changed, 4 insertions(+), 3 deletions(-)
+
+Amin Mesbah in commit 7928f578:
+ Use geohash limit defines in constraint check
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Jeffrey Lovitz in commit bb2bed78:
+ CLI Help text loop verifies arg count
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+youjiali1995 in commit 246980d0:
+ sentinel: fix randomized sentinelTimer.
+ 1 file changed, 1 insertion(+), 3 deletions(-)
+
+youjiali1995 in commit fa7de8c4:
+ bio: fix bioWaitStepOfType.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Weiliang Li in commit 7642f9d5:
+ fix usage typo in redis-cli
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0 RC5 Released Thu Sep 06 12:54:29 CEST 2018
+================================================================================
+
+Upgrade urgency HIGH: Several imporant bugs fixed.
+
+Hi all,
+
+This is the release candidate number five, and has a lot of bug fixes inside,
+together with a few big changes to the Redis behavior from the point of view
+of replication of scripts and handling of the maxmemory directive in slaves.
+Make sure to read the whole list!
+
+* BREAKING BEHAVIOR: Slaves now ignore maxmemory by default.
+* BREAKING BEHAVIOR: Now scripts are always replicated for their effects, and
+ never sending the script itself to slaves/AOF.
+* Improvement: Big pipelining performances improved significantly.
+* Fix: Rewrite BRPOPLPUSH as RPOPLPUSH to propagate.
+* Fix: False positives in tests.
+* Fix: Certain command flags were modified because not correct.
+* Fix: Fix blocking XREAD for streams that are empty.
+* Improvement: Allow scripts to timeout on slaves as well.
+* Fix: Different corner cases due to CLIENT PAUSE are now fixed.
+* Improvement: Optimize parsing large bulk greater than 32k.
+* Fix: Propagate read-only scripts as SCRIPT LOAD, not as EVAL.
+
+The following is the list of commits, so that you can read the details and
+check the credits of the commits.
+
+antirez in commit 1d1bf7f0:
+ Document that effects replication is Redis 5 default.
+ 1 file changed, 8 insertions(+)
+
+antirez in commit cfd969c7:
+ Fix scripting tests now that we default to commands repl.
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+antirez in commit 3e1fb5ff:
+ Use commands (effects) replication by default in scripts.
+ 3 files changed, 8 insertions(+), 1 deletion(-)
+
+antirez in commit c6c71abe:
+ Safer script stop condition on OOM.
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+antirez in commit dfbce91a:
+ Propagate read-only scripts as SCRIPT LOAD.
+ 1 file changed, 16 insertions(+), 3 deletions(-)
+
+antirez in commit 1705e42e:
+ Don't perform eviction when re-entering the event loop.
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+antirez in commit a0dd6f82:
+ Clarify why remaining may be zero in readQueryFromClient().
+ 1 file changed, 2 insertions(+)
+
+zhaozhao.zz in commit 2eed31a5:
+ networking: fix unexpected negative or zero readlen
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 37fb606c:
+ Merge branch '5.0' of github.com:/antirez/redis into 5.0
+zhaozhao.zz in commit 1898e6ce:
+ networking: optimize parsing large bulk greater than 32k
+ 1 file changed, 13 insertions(+), 10 deletions(-)
+
+antirez in commit 82fc63d1:
+ Unblocked clients API refactoring. See #4418.
+ 4 files changed, 33 insertions(+), 15 deletions(-)
+
+zhaozhao.zz in commit 839bb52c:
+ if master is already unblocked, do not unblock it twice
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit 2e1cd82d:
+ fix multiple unblock for clientsArePaused()
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+antirez in commit 17233080:
+ Make pending buffer processing safe for CLIENT_MASTER client.
+ 3 files changed, 22 insertions(+), 13 deletions(-)
+
+antirez in commit 42bce87a:
+ Test: processing of master stream in slave -BUSY state.
+ 1 file changed, 44 insertions(+)
+
+antirez in commit 8bf42f60:
+ After slave Lua script leaves busy state, re-process the master buffer.
+ 2 files changed, 5 insertions(+), 2 deletions(-)
+
+antirez in commit c2b104c7:
+ While the slave is busy, just accumulate master input.
+ 2 files changed, 6 insertions(+), 1 deletion(-)
+
+antirez in commit 7b75f4ae:
+ Allow scripts to timeout even if from the master instance.
+ 1 file changed, 6 insertions(+), 11 deletions(-)
+
+antirez in commit adc4e031:
+ Allow scripts to timeout on slaves as well.
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+dejun.xdj in commit 20ec1f0c:
+ Revise the comments of latency command.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+Chris Lamb in commit 8e5423eb:
+ Correct "did not received" -> "did not receive" typos/grammar.
+ 6 files changed, 10 insertions(+), 10 deletions(-)
+
+zhaozhao.zz in commit 395063d7:
+ remove duplicate bind in sentinel.conf
+ 1 file changed, 10 deletions(-)
+
+Salvatore Sanfilippo in commit b221ca41:
+ Merge pull request #5300 from SaschaRoland/xread-block-5299
+Sascha Roland in commit eea0d3c5:
+ #5299 Fix blocking XREAD for streams that ran dry
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 4cb9ee11:
+ Add maxmemory slave behavior change in the change log.
+ 1 file changed, 8 insertions(+)
+
+zhaozhao.zz in commit 5ad888ba:
+ Supplement to PR #4835, just take info/memory/command as random commands
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+zhaozhao.zz in commit d928487f:
+ some commands' flags should be set correctly, issue #4834
+ 1 file changed, 14 insertions(+), 14 deletions(-)
+
+Oran Agra in commit af675f0a:
+ Fix unstable tests on slow machines.
+ 3 files changed, 23 insertions(+), 17 deletions(-)
+
+antirez in commit f2cd16be:
+ Document slave-ignore-maxmemory in redis.conf.
+ 1 file changed, 20 insertions(+)
+
+antirez in commit 02d729b4:
+ Make slave-ignore-maxmemory configurable.
+ 1 file changed, 9 insertions(+)
+
+antirez in commit 447da44d:
+ Introduce repl_slave_ignore_maxmemory flag internally.
+ 3 files changed, 7 insertions(+)
+
+antirez in commit 868b2925:
+ Better variable meaning in processCommand().
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 319f2ee6:
+ Re-apply rebased #2358.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit 22c166da:
+ block: format code
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+zhaozhao.zz in commit c03c5913:
+ block: rewrite BRPOPLPUSH as RPOPLPUSH to propagate
+ 3 files changed, 5 insertions(+), 1 deletion(-)
+
+zhaozhao.zz in commit fcd5ef16:
+ networking: make setProtocolError simple and clear
+ 1 file changed, 11 insertions(+), 13 deletions(-)
+
+zhaozhao.zz in commit 656e4b2f:
+ networking: just move qb_pos instead of sdsrange in processInlineBuffer
+ 1 file changed, 2 insertions(+), 3 deletions(-)
+
+zhaozhao.zz in commit 2c7972ce:
+ networking: just return C_OK if multibulk processing saw a <= 0 length.
+ 1 file changed, 2 insertions(+), 5 deletions(-)
+
+zhaozhao.zz in commit 1203a04f:
+ adjust qbuf to 26 in test case for client list
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit aff86fa1:
+ pipeline: do not sdsrange querybuf unless all commands processed
+ 2 files changed, 48 insertions(+), 40 deletions(-)
+
+Chris Lamb in commit 45a6c5be:
+ Use SOURCE_DATE_EPOCH over unreproducible uname + date calls.
+ 1 file changed, 3 insertions(+)
+
+Chris Lamb in commit 186df148:
+ Make some defaults explicit in the sentinel.conf for package maintainers
+ 1 file changed, 25 insertions(+)
+
+dejun.xdj in commit b59f04a0:
+ Streams: ID of xclaim command starts from the sixth argument.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+shenlongxing in commit a3f2437b:
+ Fix stream command paras
+ 2 files changed, 7 insertions(+), 7 deletions(-)
+
+antirez in commit df911235:
+ Fix AOF comment to report the current behavior.
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+
+
+================================================================================
+Redis 5.0 RC4 Released Fri Aug 03 13:51:02 CEST 2018
+================================================================================
+
+Upgrade urgency
+
+ HIGH: Many non critical but important issues fixed.
+ CRITICAL for Stream users: Many important bugs fixed.
+
+Hi all, welcome to Redis 5.0 RC4.
+
+This release is a huge step forward in Redis 5 maturity and fixes a number
+of issues. It also provides interesting improvements. Here I'll summarize
+the biggest one, but laster you can find the full list of commits:
+
+Fixes:
+
+* A number of fixes related to Streams: stability and correctnes.
+* Fix dbRandomKey() potential infinite loop.
+* Improve eviction LFU/LRU when keys are created by INCR commands family.
+* Active defragmentation is now working on Redis 5.
+* Fix corner case in Redis CLuster / Sentinel failover, by resetting the
+ disconnection time with master in a more appropriate place.
+* Use a private version of localtime() to avoid potential deadlocks.
+* Different redis-cli non critical fixes.
+* Fix rare replication stream corruption with disk-based replication.
+
+Improvements:
+
+* Sentinel: add an option to deny online script reconfiguration.
+* Improved RESTORE command.
+* Sentinel command renaming: allows to use Sentinel with Redis instances
+ that have non standard command names.
+* CLIENT ID and CLIENT UNBLOCK.
+* CLIENT LIST now supports a TYPE option.
+* redis-cli --cluster now supports authentication.
+* redis-trib is now deprecated (use redis-cli --cluster).
+* Better slaves output buffers efficiency.
+* Faster INFO when there are many clients connected.
+* Dynamic HZ feature.
+* Improvements in what the MEMORY command is able to report.
+* Add year in log. (WARNING: may be incompatible with log scraping tools)
+* Lazy freeing now works even when values are overwritten (for instance SET).
+* Faster ZADD when elements scores are updated.
+* Improvements to the test suite, including many new options.
+
+antirez in commit a4d1201e:
+ Test suite: add --loop option.
+ 1 file changed, 12 insertions(+), 5 deletions(-)
+
+antirez in commit 273d8191:
+ Test suite: new --stop option.
+ 1 file changed, 13 insertions(+), 4 deletions(-)
+
+antirez in commit fbbcc6a6:
+ Streams IDs parsing refactoring.
+ 1 file changed, 32 insertions(+), 17 deletions(-)
+
+antirez in commit 70c4bcb7:
+ Test: new sorted set skiplist order consistency.
+ 1 file changed, 26 insertions(+)
+
+antirez in commit 63addc5c:
+ Fix zslUpdateScore() edge case.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 724740cc:
+ More commenting of zslUpdateScore().
+ 1 file changed, 2 insertions(+)
+
+antirez in commit ddc87eef:
+ Explain what's the point of zslUpdateScore() in top comment.
+ 1 file changed, 5 insertions(+)
+
+antirez in commit 741f29ea:
+ Remove old commented zslUpdateScore() from source.
+ 1 file changed, 13 deletions(-)
+
+antirez in commit 20116836:
+ Optimize zslUpdateScore() as asked in #5179.
+ 1 file changed, 44 insertions(+)
+
+antirez in commit 8c297e8b:
+ zsetAdd() refactored adding zslUpdateScore().
+ 1 file changed, 18 insertions(+), 7 deletions(-)
+
+dejun.xdj in commit bd2f3f6b:
+ Streams: rearrange the usage of '-' and '+' IDs in stream commands.
+ 1 file changed, 13 insertions(+), 13 deletions(-)
+
+dejun.xdj in commit c0c06b84:
+ Streams: add mmid_supp argument in streamParseIDOrReply().
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+antirez in commit ab237a8e:
+ Minor improvements to PR #5187.
+ 2 files changed, 13 insertions(+), 6 deletions(-)
+
+Oran Agra in commit 1ce3cf7a:
+ test suite conveniency improvements
+ 3 files changed, 79 insertions(+), 3 deletions(-)
+
+Oran Agra in commit 36622899:
+ add DEBUG LOG, to to assist test suite debugging
+ 1 file changed, 4 insertions(+)
+
+antirez in commit 83d4311a:
+ Cluster cron announce IP minor refactoring.
+ 1 file changed, 6 insertions(+), 3 deletions(-)
+
+shenlongxing in commit a633f8e1:
+ Fix cluster-announce-ip memory leak
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+antirez in commit 24c45538:
+ Tranfer -> transfer typo fixed.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+zhaozhao.zz in commit c609f240:
+ refactor dbOverwrite to make lazyfree work
+ 4 files changed, 27 insertions(+), 12 deletions(-)
+
+antirez in commit 9e971739:
+ Refactoring: replace low-level checks with writeCommandsDeniedByDiskError().
+ 2 files changed, 6 insertions(+), 13 deletions(-)
+
+antirez in commit 0e77cef0:
+ Fix writeCommandsDeniedByDiskError() inverted return value.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit acfe9d13:
+ Better top comment for writeCommandsDeniedByDiskError().
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+antirez in commit 4e933e00:
+ Introduce writeCommandsDeniedByDiskError().
+ 2 files changed, 24 insertions(+)
+
+WuYunlong in commit 41607dfd:
+ Consider aof write error as well as rdb in lua script.
+ 1 file changed, 14 insertions(+), 4 deletions(-)
+
+Salvatore Sanfilippo in commit 1d073a64:
+ Merge pull request #5168 from rpv-tomsk/issue-5033
+Guy Korland in commit 2db31fd4:
+ Few typo fixes
+ 1 file changed, 13 insertions(+), 13 deletions(-)
+
+antirez in commit 64242757:
+ Add year in log.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 50be4a1f:
+ Document dynamic-hz in the example redis.conf.
+ 1 file changed, 16 insertions(+)
+
+antirez in commit 9a76472d:
+ Make dynamic hz actually configurable.
+ 1 file changed, 9 insertions(+)
+
+antirez in commit a330d06c:
+ Control dynamic HZ via server configuration.
+ 2 files changed, 13 insertions(+), 6 deletions(-)
+
+antirez in commit d42602ff:
+ Dynamic HZ: adapt cron frequency to number of clients.
+ 2 files changed, 17 insertions(+), 5 deletions(-)
+
+antirez in commit 7b5f0223:
+ Dynamic HZ: separate hz from the configured hz.
+ 3 files changed, 15 insertions(+), 9 deletions(-)
+
+antirez in commit 037b00de:
+ Remove useless conditional from emptyDb().
+ 1 file changed, 1 deletion(-)
+
+antirez in commit 0e97ae79:
+ Make emptyDb() change introduced in #4852 simpler to read.
+ 1 file changed, 8 insertions(+), 3 deletions(-)
+
+zhaozhao.zz in commit f7740faf:
+ optimize flushdb, avoid useless loops
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+zhaozhao.zz in commit 0c008376:
+ Streams: fix xdel memory leak
+ 1 file changed, 1 insertion(+)
+
+antirez in commit dc600a25:
+ Example the magic +1 in migrateCommand().
+ 1 file changed, 4 insertions(+)
+
+antirez in commit d6827ab6:
+ Make changes of PR #5154 hopefully simpler.
+ 1 file changed, 10 insertions(+), 5 deletions(-)
+
+WuYunlong in commit 89ec1453:
+ Do not migrate already expired keys.
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+Pavel Rochnyack in commit cd25ed17:
+ INFO CPU: higher precision of reported values
+ 1 file changed, 8 insertions(+), 8 deletions(-)
+
+antirez in commit 6bfb4745:
+ Streams: refactoring of next entry seek in the iterator.
+ 1 file changed, 11 insertions(+), 7 deletions(-)
+
+zhaozhao.zz in commit 4724548e:
+ Streams: skip master fileds only when we are going forward in streamIteratorGetID
+ 1 file changed, 8 insertions(+), 5 deletions(-)
+
+Oran Agra in commit 4b79fdf1:
+ fix slave buffer test suite false positives
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit a1e081f7:
+ string2ll(): better commenting.
+ 1 file changed, 6 insertions(+)
+
+dsomeshwar in commit 8b4fe752:
+ removing redundant check
+ 1 file changed, 3 deletions(-)
+
+antirez in commit 9e5bf047:
+ Restore string2ll() to original version.
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+Oran Agra in commit c2ecdcde:
+ fix recursion typo in zmalloc_usable
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 4f742bd6:
+ string2ll(): remove duplicated check for special case.
+ 1 file changed, 1 insertion(+), 6 deletions(-)
+
+antirez in commit a4efac00:
+ string2ll(): test for NULL pointer in all the cases.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 2c07c107:
+ Change 42 to 1000 as warning level for cached scripts.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Itamar Haber in commit 270903d6:
+ Adds Lua overheads to MEMORY STATS, smartens the MEMORY DOCTOR
+ 3 files changed, 30 insertions(+), 4 deletions(-)
+
+Itamar Haber in commit faf3dbfc:
+ Adds memory information about the script's cache to INFO
+ 3 files changed, 12 insertions(+)
+
+antirez in commit 49841a54:
+ Fix merge errors.
+ 2 files changed, 7 deletions(-)
+
+antirez in commit 77a7ec72:
+ Merge branch 'unstable' into 5.0 branch
+antirez in commit 4ff47a0b:
+ Top comment clientsCron().
+ 1 file changed, 19 insertions(+), 4 deletions(-)
+
+antirez in commit aba68552:
+ Clarify that clientsCronTrackExpansiveClients() indexes may jump ahead.
+ 1 file changed, 9 insertions(+), 1 deletion(-)
+
+antirez in commit be88c0b1:
+ Rename INFO CLIENT max buffers field names for correctness.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 0cf3794e:
+ Fix wrong array index variable in getExpansiveClientsInfo().
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit ea3a20c5:
+ Change INFO CLIENTS sections to report pre-computed max/min client buffers.
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit 8f7e496b:
+ Rename var in clientsCronTrackExpansiveClients() for clarity.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+antirez in commit 8d617596:
+ Implement a function to retrieve the expansive clients mem usage.
+ 1 file changed, 12 insertions(+)
+
+antirez in commit 85a1b4f8:
+ clientsCronTrackExpansiveClients() actual implementation.
+ 1 file changed, 14 insertions(+), 1 deletion(-)
+
+antirez in commit d4c5fc57:
+ clientsCronTrackExpansiveClients() skeleton and ideas.
+ 1 file changed, 23 insertions(+)
+
+antirez in commit 1c95c075:
+ Make vars used only by INFO CLIENTS local to the block.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit 16b8d364:
+ Merge pull request #4727 from kingpeterpaule/redis-fix-info-cli
+antirez in commit 0aca977c:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 313b2240:
+ In addReplyErrorLength() only panic when replying to slave.
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+antirez in commit 6183f059:
+ Refine comment in addReplyErrorLength() about replying to masters/slaves.
+ 1 file changed, 11 insertions(+)
+
+Salvatore Sanfilippo in commit 22e9321c:
+ Merge pull request #5138 from oranagra/improve_defrag_test
+Oran Agra in commit f89c93c8:
+ make active defrag test more stable
+ 2 files changed, 6 insertions(+), 5 deletions(-)
+
+Salvatore Sanfilippo in commit 8213f64d:
+ Merge pull request #5122 from trevor211/allowWritesWhenAofDisabled
+Salvatore Sanfilippo in commit 46fd9278:
+ Merge pull request #4237 from aspirewit/update-comment
+antirez in commit 6201f7b4:
+ Streams: better error when $ is given with XREADGROUP.
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit 4bff45c7:
+ Merge pull request #5136 from 0xtonyxia/fix-xread-id-parse
+antirez in commit afc7e08a:
+ Panic when we are sending an error to our master/slave.
+ 1 file changed, 5 insertions(+)
+
+Salvatore Sanfilippo in commit e03358c0:
+ Merge pull request #5135 from oranagra/rare_repl_corruption
+dejun.xdj in commit 846cf12a:
+ Streams: remove meaningless if condition.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit 6501b6bb:
+ Streams: return an error message if using xreadgroup with '$' ID.
+ 1 file changed, 5 insertions(+)
+
+Oran Agra in commit d5559898:
+ fix rare replication stream corruption with disk-based replication
+ 3 files changed, 18 insertions(+), 9 deletions(-)
+
+antirez in commit cefe21d2:
+ dict.c: remove a few trailing spaces.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit 4fc20992:
+ Merge pull request #5128 from kingpeterpaule/remove-one-loop-in-freeMemoryIfNeeded
+Salvatore Sanfilippo in commit 9fbd49bb:
+ Merge pull request #5113 from 0xtonyxia/using-compare-func-instead
+Salvatore Sanfilippo in commit cab39676:
+ Merge pull request #5127 from oranagra/sds_req_type
+antirez in commit f9c84d6d:
+ Hopefully improve commenting of #5126.
+ 2 files changed, 22 insertions(+), 10 deletions(-)
+
+Salvatore Sanfilippo in commit e22a1218:
+ Merge pull request #5126 from oranagra/slave_buf_memory_2
+Salvatore Sanfilippo in commit 28dd8dd1:
+ Merge pull request #5132 from soloestoy/propagate-xdel-correctly
+Oran Agra in commit bf680b6f:
+ slave buffers were wasteful and incorrectly counted causing eviction
+ 10 files changed, 182 insertions(+), 50 deletions(-)
+
+zhaozhao.zz in commit 73306c6f:
+ Streams: correctly propagate xdel if needed
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+antirez in commit 103c5a1a:
+ Add a few comments to streamIteratorRemoveEntry().
+ 1 file changed, 4 insertions(+)
+
+Salvatore Sanfilippo in commit a317f55d:
+ Merge pull request #5131 from soloestoy/optimize-xdel
+antirez in commit 185e0d9c:
+ Modify XINFO field from last-id to last-generated-id.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit 4215e74b:
+ Merge pull request #5129 from soloestoy/xinfo-show-last-id
+zhaozhao.zz in commit c9324f81:
+ Streams: free lp if all elements are deleted
+ 1 file changed, 9 insertions(+), 4 deletions(-)
+
+paule in commit b6ce7d5d:
+ Update dict.c
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+zhaozhao.zz in commit b4ba5ac8:
+ Streams: show last id for streams and groups
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+peterpaule in commit 816fc6cb:
+ remove one ineffective loop in dictGetSomeKeys.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit b05a22e2:
+ bugfix in sdsReqType creating 64bit sds headers on 32bit systems
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+dejun.xdj in commit 491682a6:
+ Streams: using streamCompareID() instead of direct compare in block.c.
+ 1 file changed, 1 insertion(+), 4 deletions(-)
+
+dejun.xdj in commit a2177cd2:
+ Streams: add streamCompareID() declaration in stream.h.
+ 1 file changed, 1 insertion(+)
+
+dejun.xdj in commit 0484dbcf:
+ Streams: using streamCompareID() instead of direct compare.
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+WuYunlong in commit 2d4366c5:
+ Accept write commands if persisting is disabled, event if we do have problems persisting on disk previously.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit ab33bcd3:
+ Merge pull request #5120 from andrewsensus/leap-year-comment-patch-1
+antirez in commit 2352a519:
+ Test: XDEL fuzz testing. Remove and check stage.
+ 1 file changed, 15 insertions(+)
+
+antirez in commit 3d7d20b7:
+ Test: fix lshuffle by providing the "K" combinator.
+ 1 file changed, 2 insertions(+)
+
+antirez in commit 967ad364:
+ Test: add lshuffle in the Tcl utility functions set.
+ 1 file changed, 14 insertions(+)
+
+antirez in commit d6efd5fc:
+ Test: XDEL fuzz testing, stream creation.
+ 1 file changed, 20 insertions(+)
+
+andrewsensus in commit 8dc08ae2:
+ update leap year comment
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 69997153:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit a93f8f09:
+ Test: XDEL basic test.
+ 1 file changed, 12 insertions(+)
+
+Salvatore Sanfilippo in commit a44a5934:
+ Merge pull request #5119 from trevor211/fixSlowlogConfig
+WuYunlong in commit d6ba4fd5:
+ Fix config set slowlog-log-slower-than and condition in createLatencyReport
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+WuYunlong in commit b3660be8:
+ Add test in slowlog.tcl
+ 1 file changed, 10 insertions(+)
+
+artix in commit d4182a0a:
+ Cluster Manager: more checks on --cluster-weight option.
+ 1 file changed, 12 insertions(+), 2 deletions(-)
+
+artix in commit d222eda9:
+ Redis-trib deprecated: it no longer works and it outputs a warning to the user.
+ 1 file changed, 103 insertions(+), 1804 deletions(-)
+
+artix in commit 513eb572:
+ Cluster Manager: auth support (-a argument).
+ 1 file changed, 41 insertions(+), 19 deletions(-)
+
+Salvatore Sanfilippo in commit f3980bb9:
+ Merge pull request #5115 from shenlongxing/patch-1
+Shen Longxing in commit c2a85fb3:
+ Delete unused role checking.
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+Salvatore Sanfilippo in commit 4cb5bd4e:
+ Merge pull request #4820 from charpty/wip-serverc-simplify
+antirez in commit 8d6b7861:
+ Add regression test for #5111.
+ 1 file changed, 15 insertions(+)
+
+antirez in commit b6260a02:
+ Streams: when re-delivering because of SETID, reset deliveries counter.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit a7c180e5:
+ Simplify duplicated NACK #5112 fix.
+ 1 file changed, 18 insertions(+), 21 deletions(-)
+
+Salvatore Sanfilippo in commit bf4def0f:
+ Merge pull request #5112 from 0xtonyxia/fix-xreadgroup-crash-after-setid
+Salvatore Sanfilippo in commit 16770551:
+ Merge pull request #5114 from oranagra/defrag_32
+Oran Agra in commit 920158ec:
+ Active defrag fixes for 32bit builds (again)
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit f45e7901:
+ Merge pull request #4967 from JingchengLi/unstable
+tengfeng in commit 9505dd20:
+ fix repeat argument issue and reduce unnessary loop times for redis-cli.
+ 1 file changed, 12 insertions(+), 7 deletions(-)
+
+antirez in commit 0420c327:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 28e95c7c:
+ Streams: fix typo "consumer".
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit a8c1bb31:
+ Streams: fix new XREADGROUP sync logic.
+ 1 file changed, 13 insertions(+), 3 deletions(-)
+
+antirez in commit 1a02b5f6:
+ Streams: make blocking for > a truly special case.
+ 1 file changed, 29 insertions(+), 4 deletions(-)
+
+antirez in commit a71e8148:
+ Streams: send an error to consumers blocked on non-existing group.
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+antirez in commit 09327f11:
+ Streams: fix unblocking logic into a consumer group.
+ 1 file changed, 24 insertions(+), 14 deletions(-)
+
+dejun.xdj in commit 3f8a3efe:
+ Streams: fix xreadgroup crash after xgroup SETID is sent.
+ 1 file changed, 20 insertions(+), 15 deletions(-)
+
+Salvatore Sanfilippo in commit 7239e9ca:
+ Merge pull request #5095 from 0xtonyxia/fix-indentation
+dejun.xdj in commit 61f12973:
+ Bugfix: PEL is incorrect when consumer is blocked using xreadgroup with NOACK option.
+ 4 files changed, 6 insertions(+), 1 deletion(-)
+
+antirez in commit b67f0276:
+ redis-cli: fix #4990 additional argument in help.
+ 1 file changed, 1 insertion(+)
+
+antirez in commit 18d65849:
+ redis-cli: fix #5096 double error message.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 84620327:
+ redis-cli: cliConnect() flags CC_FORCE and CC_QUIET.
+ 1 file changed, 23 insertions(+), 13 deletions(-)
+
+Amit Dey in commit a3a5a25f:
+ fixing broken link in CONTRIBUTING
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit 289d8d9c:
+ CLIENT UNBLOCK: fix client unblock help message.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+minkikim89 in commit 62a4a8c1:
+ fix whitespace in redis-cli.c
+ 1 file changed, 362 insertions(+), 362 deletions(-)
+
+WuYunlong in commit 0a5805d7:
+ fix compile warning in addReplySubcommandSyntaxError
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit cb78c842:
+ Use nolocks_localtime() for safer logging.
+ 2 files changed, 8 insertions(+), 2 deletions(-)
+
+antirez in commit 81778d91:
+ Cache timezone and daylight active flag for safer logging.
+ 2 files changed, 14 insertions(+), 1 deletion(-)
+
+antirez in commit 18d8205b:
+ Localtime: clarify is_leap_year() working with comments.
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+antirez in commit 29644144:
+ Localtime: fix comment about leap year.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 0ea39aa4:
+ Localtime: fix daylight saving adjustment. Use * not +.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 6614d305:
+ Localtime: fix daylight time documentation and computation.
+ 1 file changed, 14 insertions(+), 4 deletions(-)
+
+dejun.xdj in commit 46f5a2ca:
+ Fix indentation.
+ 2 files changed, 14 insertions(+), 14 deletions(-)
+
+antirez in commit 243c5a7a:
+ Localtime: add a test main() function to check the output.
+ 1 file changed, 15 insertions(+), 1 deletion(-)
+
+antirez in commit c25ee35a:
+ Localtime: day of month is 1 based. Convert from 0 based "days".
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit b9f33830:
+ Localtime: fix timezone adjustment.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit 0c12cbed:
+ Localtime: compute year, month and day of the month.
+ 1 file changed, 26 insertions(+)
+
+antirez in commit 06ca400f:
+ Localtime: basics initial calculations. Year missing.
+ 1 file changed, 30 insertions(+), 2 deletions(-)
+
+antirez in commit 6a529067:
+ Localtime function skeleton and file added.
+ 1 file changed, 44 insertions(+)
+
+Jack Drogon in commit 93238575:
+ Fix typo
+ 40 files changed, 99 insertions(+), 99 deletions(-)
+
+antirez in commit 94b3ee61:
+ Clarify the pending_querybuf field of clients.
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+antirez in commit 549b8b99:
+ Improve style of PR #5084.
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit 526b30a7:
+ Merge pull request #5084 from chendq8/pending-querybuf
+antirez in commit 677d10b2:
+ Set repl_down_since to zero on state change.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit 02e38516:
+ Merge pull request #5081 from trevor211/fixClusterFailover
+chendianqiang in commit cbb2ac07:
+ Merge branch 'unstable' into pending-querybuf
+antirez in commit 2edcafb3:
+ addReplySubSyntaxError() renamed to addReplySubcommandSyntaxError().
+ 12 files changed, 14 insertions(+), 14 deletions(-)
+
+Salvatore Sanfilippo in commit bc6a0045:
+ Merge pull request #4998 from itamarhaber/module_command_help
+Salvatore Sanfilippo in commit ee09b5ed:
+ Merge pull request #5071 from akshaynagpal/patch-2
+Salvatore Sanfilippo in commit f03ad962:
+ Merge pull request #5068 from shenlongxing/fix-rename-command
+Salvatore Sanfilippo in commit e4881cd0:
+ Merge pull request #5090 from trevor211/test_helper_tcl
+WuYunlong in commit 2833cfbe:
+ fix tests/test_helper.tcl with --wait-server option. Issue #5063 added --wait-server option, but can not work.
+ 1 file changed, 1 deletion(-)
+
+chendianqiang in commit 7de1ada0:
+ limit the size of pending-querybuf in masterclient
+ 4 files changed, 48 insertions(+)
+
+WuYunlong in commit 2e167f7d:
+ fix server.repl_down_since resetting, so that slaves could failover automatically as expected.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+WuYunlong in commit aeb7bc3e:
+ cluster.tcl: Add master consecutively down test.
+ 1 file changed, 77 insertions(+)
+
+antirez in commit d751d98b:
+ Change CLIENT LIST TYPE help string.
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit a0b05a04:
+ Merge pull request #5075 from soloestoy/client-list-types
+Salvatore Sanfilippo in commit aa2c390e:
+ Merge pull request #5074 from soloestoy/fix-compile-warning
+Salvatore Sanfilippo in commit a4ef94d2:
+ Merge pull request #5076 from 0xtonyxia/add-no-auth-warning-option
+dejun.xdj in commit 9f185626:
+ Check if password is used on command line interface.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit 1139070a:
+ Fix trailing white space.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit bbd0ca95:
+ Fix code format issue.
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+dejun.xdj in commit 7becf54e:
+ Don't output password warning message when --no-auth-warning is used.
+ 1 file changed, 10 insertions(+), 1 deletion(-)
+
+dejun.xdj in commit bde05e9c:
+ Avoid -Woverlength-strings compile warning.
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+antirez in commit 5baf50d8:
+ Rax library updated (node callback).
+ 2 files changed, 19 insertions(+), 5 deletions(-)
+
+dejun.xdj in commit 0b74fd67:
+ Add --no-auth-warning help message.
+ 1 file changed, 2 insertions(+)
+
+zhaozhao.zz in commit b9cbd04b:
+ clients: add type option for client list
+ 4 files changed, 20 insertions(+), 6 deletions(-)
+
+zhaozhao.zz in commit f5538642:
+ clients: show pubsub flag in client list
+ 1 file changed, 1 insertion(+)
+
+zhaozhao.zz in commit 1fcf2737:
+ fix some compile warnings
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+Akshay Nagpal in commit 007e3cbd:
+ Added link to Google Group
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+antirez in commit ab55f9da:
+ Make CLIENT HELP output nicer to the eyes.
+ 1 file changed, 11 insertions(+), 11 deletions(-)
+
+antirez in commit 75f1a7bd:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 4a70ff74:
+ Add unblock in CLIENT HELP.
+ 1 file changed, 1 insertion(+)
+
+shenlongxing in commit 3c27db1c:
+ fix empty string for sentinel rename-command
+ 1 file changed, 5 insertions(+)
+
+Salvatore Sanfilippo in commit f7b21bc7:
+ Merge pull request #5066 from oranagra/defrag_jemalloc5_fix
+Salvatore Sanfilippo in commit 730a4cfa:
+ Merge pull request #5067 from mpaltun/mpaltun-doc-fix
+antirez in commit 2214043b:
+ CLIENT UNBLOCK: support unblocking by error.
+ 1 file changed, 22 insertions(+), 3 deletions(-)
+
+Mustafa Paltun in commit 010dc172:
+ Update t_stream.c
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Mustafa Paltun in commit 6d0acb33:
+ Update help.h
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Oran Agra in commit de495ee7:
+ minor fix in creating a stream NACK for rdb and defrag tests
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+antirez in commit 71295ee3:
+ CLIENT UNBLOCK implemented.
+ 1 file changed, 22 insertions(+)
+
+antirez in commit fb39bfd7:
+ Take clients in a ID -> Client handle dictionary.
+ 3 files changed, 6 insertions(+)
+
+antirez in commit ed65d734:
+ CLIENT ID implemented.
+ 1 file changed, 4 insertions(+)
+
+Salvatore Sanfilippo in commit 345b4809:
+ Merge pull request #5063 from oranagra/test_suite_improvements
+Salvatore Sanfilippo in commit 35c5f3fa:
+ Merge pull request #5065 from oranagra/defrag_jemalloc5
+Oran Agra in commit 5616d4c6:
+ add active defrag support for streams
+ 6 files changed, 230 insertions(+), 25 deletions(-)
+
+Oran Agra in commit e8099cab:
+ add defrag hint support into jemalloc 5
+ 3 files changed, 43 insertions(+)
+
+Oran Agra in commit 751eea24:
+ test suite infra improvements and fix
+ 2 files changed, 19 insertions(+)
+
+Salvatore Sanfilippo in commit bb666d44:
+ Merge pull request #5027 from tigertv/unstable
+antirez in commit b9058c73:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 43831779:
+ Sentinel: test command renaming feature.
+ 1 file changed, 10 insertions(+)
+
+Salvatore Sanfilippo in commit eb052ba9:
+ Merge pull request #5059 from guybe7/fix_restore_warning
+antirez in commit 27178a3f:
+ Fix type of argslen in sendSynchronousCommand().
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 1f1e724f:
+ Remove black space.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit aa5eaad4:
+ Merge pull request #5037 from madolson/repl-auth-fix
+antirez in commit 3cf8dd2c:
+ Sentinel: fix SENTINEL SET error reporting.
+ 1 file changed, 18 insertions(+), 9 deletions(-)
+
+Madelyn Olson in commit 45731edc:
+ Addressed comments
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Madelyn Olson in commit e8d68b6b:
+ Fixed replication authentication with whitespace in password
+ 1 file changed, 12 insertions(+), 5 deletions(-)
+
+antirez in commit fc0c9c80:
+ Sentinel: drop the renamed-command entry in a more natural way.
+ 1 file changed, 4 insertions(+), 7 deletions(-)
+
+antirez in commit 8ba670f5:
+ Sentinel command renaming: document it into sentinel.conf.
+ 1 file changed, 19 insertions(+)
+
+antirez in commit a8a76bda:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 2358de68:
+ Sentinel command renaming: use case sensitive hashing for the dict.
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+antirez in commit a9c50088:
+ Sentinel command renaming: fix CONFIG SET event logging.
+ 1 file changed, 17 insertions(+), 1 deletion(-)
+
+antirez in commit b72cecd7:
+ Sentinel command renaming: fix CONFIG SET after refactoring.
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+antirez in commit 91a384a5:
+ Sentinel command renaming: implement SENTINEL SET.
+ 1 file changed, 19 insertions(+)
+
+antirez in commit 903582dd:
+ Sentinel: make SENTINEL SET able to handle different arities.
+ 1 file changed, 19 insertions(+), 12 deletions(-)
+
+antirez in commit c303e768:
+ Sentinel command renaming: config rewriting.
+ 1 file changed, 12 insertions(+)
+
+antirez in commit 60df7dbe:
+ Sentinel command renaming: rename-command option parsing.
+ 1 file changed, 11 insertions(+)
+
+antirez in commit 72e8a33b:
+ Sentinel command renaming: base machanism implemented.
+ 1 file changed, 64 insertions(+), 15 deletions(-)
+
+Guy Benoish in commit dfcc20f4:
+ Fix compiler warning in restoreCommand
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit cf7fcdbe:
+ Merge pull request #4634 from soloestoy/special-auth
+Salvatore Sanfilippo in commit 70b7fa2c:
+ Merge pull request #5049 from youjiali1995/fix-load-rdb
+Salvatore Sanfilippo in commit 54d66d39:
+ Merge pull request #5053 from michael-grunder/zpopminmax-keypos
+Salvatore Sanfilippo in commit 199e704a:
+ Merge pull request #5050 from shenlongxing/fix-typo
+michael-grunder in commit db6b99f9:
+ Update ZPOPMIN/ZPOPMAX command declaration
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit a16aa03a:
+ Merge pull request #5051 from oranagra/streams_mem_estimate
+Oran Agra in commit 20e10dc7:
+ fix streams memory estimation, missing raxSeek
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+shenlongxing in commit ec55df11:
+ fix typo
+ 4 files changed, 4 insertions(+), 4 deletions(-)
+
+youjiali1995 in commit df6644fe:
+ Fix rdbLoadIntegerObject() to create shared objects when needed.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Salvatore Sanfilippo in commit 1527bcad:
+ Merge pull request #5036 from bepahol/unstable
+Salvatore Sanfilippo in commit c1e82405:
+ Merge pull request #5039 from oranagra/rdb_dbsize_hint
+Salvatore Sanfilippo in commit 79f55eed:
+ Merge pull request #5040 from oranagra/memrev64ifbe_fix
+Salvatore Sanfilippo in commit c6f4118c:
+ Merge pull request #5045 from guybe7/restore_fix
+Guy Benoish in commit b5197f1f:
+ Enhance RESTORE with RDBv9 new features
+ 5 files changed, 100 insertions(+), 22 deletions(-)
+
+Salvatore Sanfilippo in commit c6fdebf5:
+ Merge pull request #5042 from oranagra/malloc_usable_size_libc
+Oran Agra in commit 482785ac:
+ add malloc_usable_size for libc malloc
+ 2 files changed, 8 insertions(+), 3 deletions(-)
+
+Salvatore Sanfilippo in commit 4da29630:
+ Merge pull request #5023 from FX-HAO/unstable
+antirez in commit e7219025:
+ Test RDB stream encoding saving/loading.
+ 1 file changed, 17 insertions(+)
+
+Salvatore Sanfilippo in commit 5f5e1199:
+ Merge pull request #5041 from oranagra/redis-rdb-check_rdbLoadMillisecondTime
+antirez in commit 4848fbec:
+ Modules: convert hash to hash table for big objects.
+ 1 file changed, 3 insertions(+)
+
+Oran Agra in commit f31b0405:
+ fix redis-rdb-check to provide proper arguments to rdbLoadMillisecondTime
+ 2 files changed, 3 insertions(+), 2 deletions(-)
+
+antirez in commit 333c98c4:
+ AOF: remove no longer used variable "now".
+ 1 file changed, 1 deletion(-)
+
+antirez in commit e94b2053:
+ Modify clusterRedirectClient() to handle ZPOP and XREAD.
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+Oran Agra in commit 26229aa6:
+ use safe macro (non empty) in memrev64ifbe to eliminate empty if warning
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Oran Agra in commit 5cd3c952:
+ 64 bit RDB_OPCODE_RESIZEDB in rdb saving
+ 1 file changed, 3 insertions(+), 7 deletions(-)
+
+antirez in commit ba92b517:
+ Remove AOF optimization to skip expired keys.
+ 1 file changed, 3 deletions(-)
+
+Benjamin Holst in commit 36524060:
+ Update README.md
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 44571088:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit 6967d0bd:
+ Revert fix #4976 just leaving the flush() part.
+ 1 file changed, 1 insertion(+), 5 deletions(-)
+
+antirez in commit 0ed0dc3c:
+ Fix incrDecrCommand() to create shared objects when needed.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit bd92389c:
+ Refactor createObjectFromLongLong() to be suitable for value objects.
+ 2 files changed, 33 insertions(+), 2 deletions(-)
+
+Salvatore Sanfilippo in commit 3518bb66:
+ Merge pull request #5020 from shenlongxing/fix-config
+antirez in commit 20766608:
+ Streams: fix xreadGetKeys() for correctness.
+ 1 file changed, 19 insertions(+), 5 deletions(-)
+
+Salvatore Sanfilippo in commit e670ccff:
+ Merge pull request #4857 from youjiali1995/fix-command-getkeys
+antirez in commit a0b27dae:
+ Streams: fix xreadGetKeys() buffer overflow.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit 62f9ac6f:
+ Streams: Change XADD MAXLEN handling of values <= 0.
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+Max Vetrov in commit d4c4f20a:
+ Update sort.c
+ 1 file changed, 1 insertion(+), 3 deletions(-)
+
+antirez in commit 79a1c19a:
+ XADD MAXLEN should return an error for values < 0.
+ 1 file changed, 5 insertions(+)
+
+Salvatore Sanfilippo in commit 2e0ab4a8:
+ Merge pull request #4976 from trevor211/fixDebugLoadaof
+Salvatore Sanfilippo in commit 94658303:
+ Merge pull request #4758 from soloestoy/rdb-save-incremental-fsync
+antirez in commit 6a66b93b:
+ Sentinel: add an option to deny online script reconfiguration.
+ 2 files changed, 41 insertions(+)
+
+antirez in commit d353023a:
+ Merge branch 'unstable' of github.com:/antirez/redis into unstable
+antirez in commit d6e8fe77:
+ Fix infinite loop in dbRandomKey().
+ 1 file changed, 13 insertions(+)
+
+Salvatore Sanfilippo in commit 40d5df65:
+ Merge pull request #5008 from zwkno1/unstable
+Salvatore Sanfilippo in commit 8bc3ffcb:
+ Merge pull request #5021 from soloestoy/fix-exists
+Salvatore Sanfilippo in commit 6c7847a1:
+ Merge pull request #5018 from soloestoy/optimize-reply
+antirez in commit 1e92fde3:
+ Fix SCAN bug regression test, avoiding empty SREM call.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+Fuxin Hao in commit a4f658b2:
+ Fix update_zmalloc_stat_alloc in zrealloc
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0 RC3 Released Wed Jun 14 9:51:44 CEST 2018
+================================================================================
+
+Upgrade urgency LOW:
+
+This release fixes compilation of Redis RC2. For an error the commit from unstable
+updating the Rax library was not merged into the 5.0 branch.
+
+================================================================================
+Redis 5.0 RC2 Released Wed Jun 13 12:49:13 CEST 2018
+================================================================================
+
+Upgrade urgency CRITICAL: This release fixes important security issues.
+ HIGH: This release fixes a SCAN commands family bug.
+ MODERATE: This release fixes a PSYNC2 edge case with expires.
+ MODERATE: Sentinel related fixes.
+ LOW: All the other issues
+
+NOTE: This release breaks API compatibility with certain APIs that were
+introduced in Redis 5.0 RC1. Notably ZPOP* now returns score/element in reverse
+order. XINFO <key> special form was removed, now XINFO STREAM <key> must be
+used to obtain general information about the stream.
+
+Redis 5.0 RC2 ixes a number of important issues:
+
+* Important security issues related to the Lua scripting engine.
+ Please check https://github.com/antirez/redis/issues/5017
+ for more information.
+
+* A bug with SCAN, SSCAN, HSCAN and ZSCAN, that may not return all the elements.
+ We also add a regression test that can trigger the issue often when present, and
+ may in theory be able to find unrelated regressions.
+
+* A PSYNC2 bug is fixed: Redis should not expire keys when saving RDB files
+ because otherwise it is no longer possible to use such RDB file as a base
+ for partial resynchronization. It no longer represents the right state.
+
+* Compatibility of AOF with RDB preamble when the RDB checksum is disabled.
+
+* Sentinel bug that in some cases prevented Sentinel to detect that the master
+ was down immediately. A delay was added to the detection.
+
+* XREADGROUP would crash when the master had attacked slaves.
+
+* Replication and events generation of several streams command were fixed.
+
+* XREVRANGE fixed, in some cases it could not return elements, or crash the
+ server, or in general not behave correctly.
+
+* ZPOP can now unblock multiple clients in a sane way.
+
+* Other minor issues.
+
+Moreover this release adds new features:
+
+* XGROUP DESTROY and XGROUP SETID.
+
+* RDB loading speedup.
+
+* Configurable stream macro node limits (number of elements / bytes).
+
+* More smaller improvements.
+
+The following is the list of commits composing the release, please check
+the commit messages and authors for credits.
+
+antirez in commit 9fdcc159:
+ Security: fix redis-cli buffer overflow.
+ 1 file changed, 16 insertions(+), 11 deletions(-)
+
+antirez in commit cf760071:
+ Security: fix Lua struct package offset handling.
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+antirez in commit a57595ca:
+ Security: more cmsgpack fixes by @soloestoy.
+ 1 file changed, 7 insertions(+)
+
+antirez in commit 8783fb94:
+ Security: update Lua struct package for security.
+ 1 file changed, 23 insertions(+), 23 deletions(-)
+
+antirez in commit 8cb9344b:
+ Security: fix Lua cmsgpack library stack overflow.
+ 1 file changed, 3 insertions(+)
+
+赵磊 in commit 59080f60:
+ Fix dictScan(): It can't scan all buckets when dict is shrinking.
+ 1 file changed, 14 insertions(+), 11 deletions(-)
+
+dejun.xdj in commit ac2a824a:
+ Fix redis-cli memory leak when sending set preference command.
+ 1 file changed, 2 insertions(+)
+
+dejun.xdj in commit c7197ff5:
+ Check if the repeat value is positive in while loop of cliSendCommand().
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit 3f77777f:
+ Change the type of repeat argument to long for function cliSendCommand.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit 7a565d72:
+ Fix negtive repeat command value issue.
+ 1 file changed, 11 insertions(+), 3 deletions(-)
+
+dejun.xdj in commit 64bf60fb:
+ Detect and stop saving history for auth command with repeat option.
+ 1 file changed, 17 insertions(+), 10 deletions(-)
+
+dejun.xdj in commit 5bed12aa:
+ Change the warning message a little bit to avoid trademark issuses.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+dejun.xdj in commit d71c4961:
+ Stop saving auth command in redis-cli history.
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+dejun.xdj in commit fca99e41:
+ Add warning message when using password on command line
+ 1 file changed, 1 insertion(+)
+
+antirez in commit 01407a3a:
+ Don't expire keys while loading RDB from AOF preamble.
+ 3 files changed, 5 insertions(+), 5 deletions(-)
+
+WuYunlong in commit fb5408cf:
+ Fix rdb save by allowing dumping of expire keys, so that when we add a new slave, and do a failover, eighter by manual or not, other local slaves will delete the expired keys properly.
+ 2 files changed, 3 insertions(+), 7 deletions(-)
+
+antirez in commit 0b8b6df4:
+ Backport hiredis issue 525 fix to compile on FreeBSD.
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+antirez in commit e98627c5:
+ Add INIT INFO to the provided init script.
+ 1 file changed, 8 insertions(+)
+
+antirez in commit 17f5de89:
+ Fix ae.c when a timer finalizerProc adds an event.
+ 2 files changed, 10 insertions(+), 6 deletions(-)
+
+antirez in commit 266e6423:
+ Sentinel: fix delay in detecting ODOWN.
+ 1 file changed, 9 insertions(+), 5 deletions(-)
+
+zhaozhao.zz in commit eafaf172:
+ AOF & RDB: be compatible with rdbchecksum no
+ 1 file changed, 9 insertions(+), 7 deletions(-)
+
+huijing.whj in commit 4630da37:
+ fix int overflow problem in freeMemoryIfNeeded
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+================================================================================
+Redis 5.0 RC1 Released Tue May 29 14:14:11 CEST 2018
+================================================================================
+
+Upgrade urgency LOW: This is the first RC of Redis 5.
+
+Introduction to the Redis 5 release
+===================================
+
+Redis 5 is a release focused on a few important features. While Redis 4
+was very very focused on operations, Redis 5 changes are mostly user-facing,
+with the implementation of new data types and operations on top of existing
+types. The following are the major features of this release:
+
+1. The new Stream data type. https://redis.io/topics/streams-intro
+2. New Redis modules APIs: Timers, Cluster and Dictionary APIs.
+3. RDB now store LFU and LRU information.
+4. The cluster manager was ported from Ruby (redis-trib.rb) to C code
+ inside redis-cli. Check `redis-cli --cluster help` for more info.
+5. New sorted set commands: ZPOPMIN/MAX and blocking variants.
+6. Active defragmentation version 2.
+7. Improvemenets in HyperLogLog implementations.
+8. Better memory reporting capabilities.
+9. Many commands with sub-commands now have an HELP subcommand.
+10. Better performances when clients connect and disconnect often.
+11. Many bug fixes and other random improvements.
+12. Jemalloc was upgraded to version 5.1
+13. CLIENT UNBLOCK and CLIENT ID.
+14. The LOLWUT command was added. http://antirez.com/news/123
+15. We no longer use the "slave" word if not for API backward compatibility.
+16. Differnet optimizations in the networking layer.
+17. Lua improvements:
+ - Better propagation of Lua scripts to slaves / AOF.
+ - Lua scripts can now timeout and get in -BUSY state in the slave as well.
+18. Dynamic HZ to balance idle CPU usage with responsiveness.
+19. The Redis core was refactored and improved in many ways.
+
+Thanks to all the users and developers who made this release possible.
+We'll follow up with more RC releases, until the code looks production ready
+and we don't get reports of serious issues for a while.
+
+A special thank you for the amount of work put into this release
+(in decreasing number of commits) by:
+
+Fabio Nicotra, <artix2@gmail.com>
+Soloestoy <zhaozhao.zz@alibaba-inc.com>
+Itamar Haber <itamar@redislabs.com>
+Oran Agra <oran@redislabs.com>
+Dvir Volk <dvirsky@gmail.com>
+dejun.xdj <dejun.xdj@alibaba-inc.com>
+Guy Benoish <guy.benoish@redislabs.com>
+Charsyam <charsyam@gmail.com>
+Otmar Ertl <otmar.ertl@gmail.com>
+Jan-Erik Rediger <janerik@fnordig.de>
+Spinlock <wnzheng@gmail.com>
+
+Migrating from 4.0 to 5.0
+=========================
+
+Redis 4.0 is mostly a strict subset of 5.0, you should not have any problem
+upgrading your application from 4.0 to 5.0. However this is a list of small
+non-backward compatible changes introduced in the 5.0 release:
+
+* redis-cli now implements the cluster management tool. We still ship the
+ old redis-trib, but new fixes will be implemented only in redis-cli.
+ See `redis-cli --cluster help` for more info.
+
+* The RDB format changed. Redis 5.0 is still able to read 4.0 (and all the
+ past versions) files, but not the other way around.
+
+* Certain log formats and sentences are different in Redis 5.0.
+
+* Now by default maxmemory is ignored by slaves, and used only once a slave
+ is promoted to master. It means that in setups where you want slaves to
+ enforce maxmemory in an independent way from the master (that will anyway
+ stream the key eviction DEL commands), you should active this feature manually
+ and make sure you understand that it breaks consistency if writes are not
+ always idempotent. TLDR: the new behavior is much better for 99.999% of use
+ cases, revert it if you really know what you are doing.
+
+* Scripts are only replicated by their *effects* and not by sending EVAL/EVALSHA
+ to slaves or the AOF log itself. This is much better in the general case
+ and in the future we want to totally remove the other possiblity of
+ propagating scripts the old way (as EVAL). However you can still turn this
+ back to the default via the non-documented (if not here) Redis configuration
+ directive "lua-replicate-commands yes" or
+ "DEBUG lua-always-replicate-commands 0". However note that Redis 6 may
+ completely remove such feature.
+
+* Because of the above change related to scripts replication, certain Redis
+ commands that in Redis 4 had their result ordered lexicographically before
+ being passed to Lua via the return value of redis.call(), now have a behavior
+ more similar to calling the commands directly from a normal client. For
+ instance the ordering of elements returned by SMEMBERS or SDIFF is now
+ undetermined in Lua, exactly as it is by default when calling the commands
+ from a non-scripting context.
+
+--------------------------------------------------------------------------------
+
+Credits: For each release, a list of changes with the relative author is
+provided. Where not specified the implementation and design is done by
+Salvatore Sanfilippo. Thanks to Redis Labs for making all this possible.
+Also many thanks to all the other contributors and the amazing community
+we have.
+
+Commit messages may contain additional credits.
+
+Enjoy,
+Salvatore
diff --git a/Redis-x64-5.0.14.1/EventLog.dll b/Redis-x64-5.0.14.1/EventLog.dll
new file mode 100644
index 0000000..3cf1453
--- /dev/null
+++ b/Redis-x64-5.0.14.1/EventLog.dll
Binary files differ
diff --git a/Redis-x64-5.0.14.1/README.txt b/Redis-x64-5.0.14.1/README.txt
new file mode 100644
index 0000000..b06e115
--- /dev/null
+++ b/Redis-x64-5.0.14.1/README.txt
@@ -0,0 +1,19 @@
+Redis for Windows - https://github.com/tporadowski/redis
+========================================================
+
+This version of Redis (https://redis.io/) is an unofficial port to Windows OS
+based on work contributed by Microsoft Open Technologies Inc. It is maintained
+by Tomasz Poradowski (tomasz@poradowski.com, http://www.poradowski.com/en/).
+
+Contents of this package:
+- *.exe - various Redis for Windows executables compiled for x64 platfrom,
+- *.pdb - accompanying PDB files useful for debugging purposes,
+- license.txt - license information (BSD-like),
+- RELEASENOTES.txt - Windows-specific release notes,
+- 00-RELEASENOTES - changelog of original Redis project, those changes are
+ ported back to this Windows version.
+
+For more information - please visit https://github.com/tporadowski/redis
+
+If you find this version of Redis useful and would like to support ongoing
+development - please consider sponsoring my work at https://github.com/sponsors/tporadowski
diff --git a/Redis-x64-5.0.14.1/RELEASENOTES.txt b/Redis-x64-5.0.14.1/RELEASENOTES.txt
new file mode 100644
index 0000000..bdcebc6
--- /dev/null
+++ b/Redis-x64-5.0.14.1/RELEASENOTES.txt
@@ -0,0 +1,99 @@
+Redis for Windows - https://github.com/tporadowski/redis
+========================================================
+
+This file provides information about Windows-specific changes to Redis.
+For release notes related to original Redis project - please see 00-RELEASENOTES.
+
+--------------------------------------------------------
+
+2021-10-18: Redis 5.0.14 for Windows
+https://github.com/tporadowski/redis/releases/tag/v5.0.14
+
+Bugfix/maintenance release of Redis for Windows, updated to be in sync with
+redis/5.0.14 (https://github.com/redis/redis/releases/tag/5.0.14).
+Additionally "SCRIPT DEBUG SYNC" is now available.
+
+--------------------------------------------------------
+
+2020-11-08: Redis 5.0.10 for Windows
+https://github.com/tporadowski/redis/releases/tag/v5.0.10
+
+Bugfix/maintenance release of Redis for Windows, updated to be in sync with
+redis/5.0.10 (https://github.com/redis/redis/releases/tag/5.0.10)
+
+NOTE: active memory defragmentation feature ("activedefrag" option) is turned OFF.
+
+--------------------------------------------------------
+
+2020-05-02: Redis 5.0.9 for Windows
+https://github.com/tporadowski/redis/releases/tag/v5.0.9
+
+First release of Redis 5.x for Windows, updated to be in sync with antirez/5.0.9.
+
+--------------------------------------------------------
+
+2020-01-26: Redis 4.0.14.2 for Windows
+https://github.com/tporadowski/redis/releases/tag/v4.0.14.2
+
+This is a hotfix release of 4.0.14 branch that fixes #50 related to running in
+Sentinel mode.
+
+--------------------------------------------------------
+
+2020-01-15: Redis 4.0.14.1 for Windows
+https://github.com/tporadowski/redis/releases/tag/v4.0.14.1
+
+This is a hotfix release of 4.0.14 branch that fixes 2 Windows-specific issues:
+
+ * #46 - added support for older Windows versions (prior Windows 8 and Windows Server 2012)
+ * #47 - fixed problem with parsing command-line arguments.
+
+--------------------------------------------------------
+
+2019-08-29: Redis 4.0.14 for Windows
+https://github.com/tporadowski/redis/releases/tag/v4.0.14
+
+Redis 4.0.14 for Windows is a merge of Windows-specific changes from latest (unsupported) 3.2.100 release from MSOpenTech and original Redis 4.0.14.
+
+--------------------------------------------------------
+
+2018-10-01: Redis for Windows 4.0.2.3 (alpha)
+
+This 4.0.2.3 release is still an alpha version, but contains enhancements and fixes for:
+
+ * #14: decrease logging verbosity of some cluster-related messages
+ * #23: ZRANK/ZREVRANK bugfix (win-port only)
+ * failed unit tests (bdcf80e).
+
+--------------------------------------------------------
+
+2018-03-26: Redis for Windows 4.0.2.2 (alpha)
+https://github.com/tporadowski/redis/releases/tag/v4.0.2.2-alpha
+
+This 4.0.2.2 release is still an alpha version, but contains a fix to issue #12
+(crash when rewriting AOF file - this issue was specific to Windows port only).
+
+--------------------------------------------------------
+
+2018-03-17: Redis for Windows 4.0.2.1 (alpha)
+https://github.com/tporadowski/redis/releases/tag/v4.0.2.1-alpha
+
+This 4.0.2.1 release is still an alpha version, but contains a fix to issue #11,
+which was related to sending back larger amounts of data to Redis clients
+(this issue was specific to Windows port only).
+
+--------------------------------------------------------
+
+2017-11-22: Redis 4.0.2 for Windows (alpha)
+https://github.com/tporadowski/redis/releases/tag/v4.0.2-alpha
+
+Alpha version of Redis 4.0.2 for Windows.
+
+Redis 4.0.2 for Windows is a merge of Windows-specific changes from latest (unsupported) 3.2.100 release
+from MSOpenTech and Redis 4.0.2 and this alpha release consists of:
+
+ * all Redis 4.0.2 features except modules,
+ * all executables of Redis (redis-server, redis-cli, redis-benchmark, redis-check-aof, redis-check-rdb).
+
+Main difference to official Redis 4.0.2 (except no support for modules at the moment) is old version
+of jemalloc-win dependency, which is planned to be updated to the same version in beta release.
diff --git a/Redis-x64-5.0.14.1/dump.rdb b/Redis-x64-5.0.14.1/dump.rdb
new file mode 100644
index 0000000..d4a26e8
--- /dev/null
+++ b/Redis-x64-5.0.14.1/dump.rdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-benchmark.exe b/Redis-x64-5.0.14.1/redis-benchmark.exe
new file mode 100644
index 0000000..b7e27b9
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-benchmark.exe
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-benchmark.pdb b/Redis-x64-5.0.14.1/redis-benchmark.pdb
new file mode 100644
index 0000000..0682a66
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-benchmark.pdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-check-aof.exe b/Redis-x64-5.0.14.1/redis-check-aof.exe
new file mode 100644
index 0000000..5b1b425
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-check-aof.exe
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-check-aof.pdb b/Redis-x64-5.0.14.1/redis-check-aof.pdb
new file mode 100644
index 0000000..0d5726e
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-check-aof.pdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-check-rdb.exe b/Redis-x64-5.0.14.1/redis-check-rdb.exe
new file mode 100644
index 0000000..5b1b425
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-check-rdb.exe
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-check-rdb.pdb b/Redis-x64-5.0.14.1/redis-check-rdb.pdb
new file mode 100644
index 0000000..0d5726e
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-check-rdb.pdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-cli.exe b/Redis-x64-5.0.14.1/redis-cli.exe
new file mode 100644
index 0000000..40a73ba
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-cli.exe
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-cli.pdb b/Redis-x64-5.0.14.1/redis-cli.pdb
new file mode 100644
index 0000000..121bae1
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-cli.pdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-server.exe b/Redis-x64-5.0.14.1/redis-server.exe
new file mode 100644
index 0000000..5b1b425
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-server.exe
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis-server.pdb b/Redis-x64-5.0.14.1/redis-server.pdb
new file mode 100644
index 0000000..0d5726e
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis-server.pdb
Binary files differ
diff --git a/Redis-x64-5.0.14.1/redis.windows-service.conf b/Redis-x64-5.0.14.1/redis.windows-service.conf
new file mode 100644
index 0000000..0b6b1da
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis.windows-service.conf
@@ -0,0 +1,1336 @@
+# Redis configuration file example
+
+# Note on units: when memory size is needed, it is possible to specify
+# it in the usual form of 1k 5GB 4M and so forth:
+#
+# 1k => 1000 bytes
+# 1kb => 1024 bytes
+# 1m => 1000000 bytes
+# 1mb => 1024*1024 bytes
+# 1g => 1000000000 bytes
+# 1gb => 1024*1024*1024 bytes
+#
+# units are case insensitive so 1GB 1Gb 1gB are all the same.
+
+################################## INCLUDES ###################################
+
+# Include one or more other config files here. This is useful if you
+# have a standard template that goes to all Redis servers but also need
+# to customize a few per-server settings. Include files can include
+# other files, so use this wisely.
+#
+# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
+# from admin or Redis Sentinel. Since Redis always uses the last processed
+# line as value of a configuration directive, you'd better put includes
+# at the beginning of this file to avoid overwriting config change at runtime.
+#
+# If instead you are interested in using includes to override configuration
+# options, it is better to use include as the last line.
+#
+# include .\path\to\local.conf
+# include c:\path\to\other.conf
+
+################################## MODULES #####################################
+
+# Load modules at startup. If the server is not able to load modules
+# it will abort. It is possible to use multiple loadmodule directives.
+#
+# loadmodule .\path\to\my_module.dll
+# loadmodule c:\path\to\other_module.dll
+
+################################## NETWORK #####################################
+
+# By default, if no "bind" configuration directive is specified, Redis listens
+# for connections from all the network interfaces available on the server.
+# It is possible to listen to just one or multiple selected interfaces using
+# the "bind" configuration directive, followed by one or more IP addresses.
+#
+# Examples:
+#
+# bind 192.168.1.100 10.0.0.1
+# bind 127.0.0.1 ::1
+#
+# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
+# internet, binding to all the interfaces is dangerous and will expose the
+# instance to everybody on the internet. So by default we uncomment the
+# following bind directive, that will force Redis to listen only into
+# the IPv4 loopback interface address (this means Redis will be able to
+# accept connections only from clients running into the same computer it
+# is running).
+#
+# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
+# JUST COMMENT THE FOLLOWING LINE.
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+bind 127.0.0.1
+
+# Protected mode is a layer of security protection, in order to avoid that
+# Redis instances left open on the internet are accessed and exploited.
+#
+# When protected mode is on and if:
+#
+# 1) The server is not binding explicitly to a set of addresses using the
+# "bind" directive.
+# 2) No password is configured.
+#
+# The server only accepts connections from clients connecting from the
+# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
+# sockets.
+#
+# By default protected mode is enabled. You should disable it only if
+# you are sure you want clients from other hosts to connect to Redis
+# even if no authentication is configured, nor a specific set of interfaces
+# are explicitly listed using the "bind" directive.
+protected-mode yes
+
+# Accept connections on the specified port, default is 6379 (IANA #815344).
+# If port 0 is specified Redis will not listen on a TCP socket.
+port 6379
+
+# TCP listen() backlog.
+#
+# In high requests-per-second environments you need an high backlog in order
+# to avoid slow clients connections issues. Note that the Linux kernel
+# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
+# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
+# in order to get the desired effect.
+tcp-backlog 511
+
+# Unix socket.
+#
+# Specify the path for the Unix socket that will be used to listen for
+# incoming connections. There is no default, so Redis will not listen
+# on a unix socket when not specified.
+#
+# unixsocket /tmp/redis.sock
+# unixsocketperm 700
+
+# Close the connection after a client is idle for N seconds (0 to disable)
+timeout 0
+
+# TCP keepalive.
+#
+# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
+# of communication. This is useful for two reasons:
+#
+# 1) Detect dead peers.
+# 2) Take the connection alive from the point of view of network
+# equipment in the middle.
+#
+# On Linux, the specified value (in seconds) is the period used to send ACKs.
+# Note that to close the connection the double of the time is needed.
+# On other kernels the period depends on the kernel configuration.
+#
+# A reasonable value for this option is 300 seconds, which is the new
+# Redis default starting with Redis 3.2.1.
+tcp-keepalive 300
+
+################################# GENERAL #####################################
+
+# By default Redis does not run as a daemon. Use 'yes' if you need it.
+# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
+# NOT SUPPORTED ON WINDOWS daemonize no
+
+# If you run Redis from upstart or systemd, Redis can interact with your
+# supervision tree. Options:
+# supervised no - no supervision interaction
+# supervised upstart - signal upstart by putting Redis into SIGSTOP mode
+# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
+# supervised auto - detect upstart or systemd method based on
+# UPSTART_JOB or NOTIFY_SOCKET environment variables
+# Note: these supervision methods only signal "process is ready."
+# They do not enable continuous liveness pings back to your supervisor.
+# NOT SUPPORTED ON WINDOWS supervised no
+
+# If a pid file is specified, Redis writes it where specified at startup
+# and removes it at exit.
+#
+# When the server runs non daemonized, no pid file is created if none is
+# specified in the configuration. When the server is daemonized, the pid file
+# is used even if not specified, defaulting to "/var/run/redis.pid".
+#
+# Creating a pid file is best effort: if Redis is not able to create it
+# nothing bad happens, the server will start and run normally.
+# NOT SUPPORTED ON WINDOWS pidfile /var/run/redis.pid
+
+# Specify the server verbosity level.
+# This can be one of:
+# debug (a lot of information, useful for development/testing)
+# verbose (many rarely useful info, but not a mess like the debug level)
+# notice (moderately verbose, what you want in production probably)
+# warning (only very important / critical messages are logged)
+loglevel notice
+
+# Specify the log file name. Also 'stdout' can be used to force
+# Redis to log on the standard output.
+logfile "server_log.txt"
+
+# To enable logging to the Windows EventLog, just set 'syslog-enabled' to
+# yes, and optionally update the other syslog parameters to suit your needs.
+# If Redis is installed and launched as a Windows Service, this will
+# automatically be enabled.
+syslog-enabled yes
+
+# Specify the source name of the events in the Windows Application log.
+syslog-ident redis
+
+# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
+# NOT SUPPORTED ON WINDOWS syslog-facility local0
+
+# Set the number of databases. The default database is DB 0, you can select
+# a different one on a per-connection basis using SELECT <dbid> where
+# dbid is a number between 0 and 'databases'-1
+databases 16
+
+# By default Redis shows an ASCII art logo only when started to log to the
+# standard output and if the standard output is a TTY. Basically this means
+# that normally a logo is displayed only in interactive sessions.
+#
+# However it is possible to force the pre-4.0 behavior and always show a
+# ASCII art logo in startup logs by setting the following option to yes.
+always-show-logo yes
+
+################################ SNAPSHOTTING ################################
+#
+# Save the DB on disk:
+#
+# save <seconds> <changes>
+#
+# Will save the DB if both the given number of seconds and the given
+# number of write operations against the DB occurred.
+#
+# In the example below the behaviour will be to save:
+# after 900 sec (15 min) if at least 1 key changed
+# after 300 sec (5 min) if at least 10 keys changed
+# after 60 sec if at least 10000 keys changed
+#
+# Note: you can disable saving completely by commenting out all "save" lines.
+#
+# It is also possible to remove all the previously configured save
+# points by adding a save directive with a single empty string argument
+# like in the following example:
+#
+# save ""
+
+save 900 1
+save 300 10
+save 60 10000
+
+# By default Redis will stop accepting writes if RDB snapshots are enabled
+# (at least one save point) and the latest background save failed.
+# This will make the user aware (in a hard way) that data is not persisting
+# on disk properly, otherwise chances are that no one will notice and some
+# disaster will happen.
+#
+# If the background saving process will start working again Redis will
+# automatically allow writes again.
+#
+# However if you have setup your proper monitoring of the Redis server
+# and persistence, you may want to disable this feature so that Redis will
+# continue to work as usual even if there are problems with disk,
+# permissions, and so forth.
+stop-writes-on-bgsave-error yes
+
+# Compress string objects using LZF when dump .rdb databases?
+# For default that's set to 'yes' as it's almost always a win.
+# If you want to save some CPU in the saving child set it to 'no' but
+# the dataset will likely be bigger if you have compressible values or keys.
+rdbcompression yes
+
+# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
+# This makes the format more resistant to corruption but there is a performance
+# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
+# for maximum performances.
+#
+# RDB files created with checksum disabled have a checksum of zero that will
+# tell the loading code to skip the check.
+rdbchecksum yes
+
+# The filename where to dump the DB
+dbfilename dump.rdb
+
+# The working directory.
+#
+# The DB will be written inside this directory, with the filename specified
+# above using the 'dbfilename' configuration directive.
+#
+# The Append Only File will also be created inside this directory.
+#
+# Note that you must specify a directory here, not a file name.
+dir ./
+
+################################# REPLICATION #################################
+
+# Master-Replica replication. Use replicaof to make a Redis instance a copy of
+# another Redis server. A few things to understand ASAP about Redis replication.
+#
+# +------------------+ +---------------+
+# | Master | ---> | Replica |
+# | (receive writes) | | (exact copy) |
+# +------------------+ +---------------+
+#
+# 1) Redis replication is asynchronous, but you can configure a master to
+# stop accepting writes if it appears to be not connected with at least
+# a given number of replicas.
+# 2) Redis replicas are able to perform a partial resynchronization with the
+# master if the replication link is lost for a relatively small amount of
+# time. You may want to configure the replication backlog size (see the next
+# sections of this file) with a sensible value depending on your needs.
+# 3) Replication is automatic and does not need user intervention. After a
+# network partition replicas automatically try to reconnect to masters
+# and resynchronize with them.
+#
+# replicaof <masterip> <masterport>
+
+# If the master is password protected (using the "requirepass" configuration
+# directive below) it is possible to tell the replica to authenticate before
+# starting the replication synchronization process, otherwise the master will
+# refuse the replica request.
+#
+# masterauth <master-password>
+
+# When a replica loses its connection with the master, or when the replication
+# is still in progress, the replica can act in two different ways:
+#
+# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will
+# still reply to client requests, possibly with out of date data, or the
+# data set may just be empty if this is the first synchronization.
+#
+# 2) if replica-serve-stale-data is set to 'no' the replica will reply with
+# an error "SYNC with master in progress" to all the kind of commands
+# but to INFO, replicaOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG,
+# SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB,
+# COMMAND, POST, HOST: and LATENCY.
+#
+replica-serve-stale-data yes
+
+# You can configure a replica instance to accept writes or not. Writing against
+# a replica instance may be useful to store some ephemeral data (because data
+# written on a replica will be easily deleted after resync with the master) but
+# may also cause problems if clients are writing to it because of a
+# misconfiguration.
+#
+# Since Redis 2.6 by default replicas are read-only.
+#
+# Note: read only replicas are not designed to be exposed to untrusted clients
+# on the internet. It's just a protection layer against misuse of the instance.
+# Still a read only replica exports by default all the administrative commands
+# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
+# security of read only replicas using 'rename-command' to shadow all the
+# administrative / dangerous commands.
+replica-read-only yes
+
+# Replication SYNC strategy: disk or socket.
+#
+# -------------------------------------------------------
+# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY
+# -------------------------------------------------------
+#
+# New replicas and reconnecting replicas that are not able to continue the replication
+# process just receiving differences, need to do what is called a "full
+# synchronization". An RDB file is transmitted from the master to the replicas.
+# The transmission can happen in two different ways:
+#
+# 1) Disk-backed: The Redis master creates a new process that writes the RDB
+# file on disk. Later the file is transferred by the parent
+# process to the replicas incrementally.
+# 2) Diskless: The Redis master creates a new process that directly writes the
+# RDB file to replica sockets, without touching the disk at all.
+#
+# With disk-backed replication, while the RDB file is generated, more replicas
+# can be queued and served with the RDB file as soon as the current child producing
+# the RDB file finishes its work. With diskless replication instead once
+# the transfer starts, new replicas arriving will be queued and a new transfer
+# will start when the current one terminates.
+#
+# When diskless replication is used, the master waits a configurable amount of
+# time (in seconds) before starting the transfer in the hope that multiple replicas
+# will arrive and the transfer can be parallelized.
+#
+# With slow disks and fast (large bandwidth) networks, diskless replication
+# works better.
+repl-diskless-sync no
+
+# When diskless replication is enabled, it is possible to configure the delay
+# the server waits in order to spawn the child that transfers the RDB via socket
+# to the replicas.
+#
+# This is important since once the transfer starts, it is not possible to serve
+# new replicas arriving, that will be queued for the next RDB transfer, so the server
+# waits a delay in order to let more replicas arrive.
+#
+# The delay is specified in seconds, and by default is 5 seconds. To disable
+# it entirely just set it to 0 seconds and the transfer will start ASAP.
+repl-diskless-sync-delay 5
+
+# Replicas send PINGs to server in a predefined interval. It's possible to change
+# this interval with the repl_ping_replica_period option. The default value is 10
+# seconds.
+#
+# repl-ping-replica-period 10
+
+# The following option sets the replication timeout for:
+#
+# 1) Bulk transfer I/O during SYNC, from the point of view of replica.
+# 2) Master timeout from the point of view of replicas (data, pings).
+# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).
+#
+# It is important to make sure that this value is greater than the value
+# specified for repl-ping-replica-period otherwise a timeout will be detected
+# every time there is low traffic between the master and the replica.
+#
+# repl-timeout 60
+
+# Disable TCP_NODELAY on the replica socket after SYNC?
+#
+# If you select "yes" Redis will use a smaller number of TCP packets and
+# less bandwidth to send data to replicas. But this can add a delay for
+# the data to appear on the replica side, up to 40 milliseconds with
+# Linux kernels using a default configuration.
+#
+# If you select "no" the delay for data to appear on the replica side will
+# be reduced but more bandwidth will be used for replication.
+#
+# By default we optimize for low latency, but in very high traffic conditions
+# or when the master and replicas are many hops away, turning this to "yes" may
+# be a good idea.
+repl-disable-tcp-nodelay no
+
+# Set the replication backlog size. The backlog is a buffer that accumulates
+# replica data when replicas are disconnected for some time, so that when a replica
+# wants to reconnect again, often a full resync is not needed, but a partial
+# resync is enough, just passing the portion of data the replica missed while
+# disconnected.
+#
+# The bigger the replication backlog, the longer the time the replica can be
+# disconnected and later be able to perform a partial resynchronization.
+#
+# The backlog is only allocated once there is at least a replica connected.
+#
+# repl-backlog-size 1mb
+
+# After a master has no longer connected replicas for some time, the backlog
+# will be freed. The following option configures the amount of seconds that
+# need to elapse, starting from the time the last replica disconnected, for
+# the backlog buffer to be freed.
+#
+# Note that replicas never free the backlog for timeout, since they may be
+# promoted to masters later, and should be able to correctly "partially
+# resynchronize" with the replicas: hence they should always accumulate backlog.
+#
+# A value of 0 means to never release the backlog.
+#
+# repl-backlog-ttl 3600
+
+# The replica priority is an integer number published by Redis in the INFO output.
+# It is used by Redis Sentinel in order to select a replica to promote into a
+# master if the master is no longer working correctly.
+#
+# A replica with a low priority number is considered better for promotion, so
+# for instance if there are three replicas with priority 10, 100, 25 Sentinel will
+# pick the one with priority 10, that is the lowest.
+#
+# However a special priority of 0 marks the replica as not able to perform the
+# role of master, so a replica with priority of 0 will never be selected by
+# Redis Sentinel for promotion.
+#
+# By default the priority is 100.
+replica-priority 100
+
+# It is possible for a master to stop accepting writes if there are less than
+# N replicas connected, having a lag less or equal than M seconds.
+#
+# The N replicas need to be in "online" state.
+#
+# The lag in seconds, that must be <= the specified value, is calculated from
+# the last ping received from the replica, that is usually sent every second.
+#
+# This option does not GUARANTEE that N replicas will accept the write, but
+# will limit the window of exposure for lost writes in case not enough replicas
+# are available, to the specified number of seconds.
+#
+# For example to require at least 3 replicas with a lag <= 10 seconds use:
+#
+# min-replicas-to-write 3
+# min-replicas-max-lag 10
+#
+# Setting one or the other to 0 disables the feature.
+#
+# By default min-replicas-to-write is set to 0 (feature disabled) and
+# min-replicas-max-lag is set to 10.
+
+# A Redis master is able to list the address and port of the attached
+# replicas in different ways. For example the "INFO replication" section
+# offers this information, which is used, among other tools, by
+# Redis Sentinel in order to discover replica instances.
+# Another place where this info is available is in the output of the
+# "ROLE" command of a master.
+#
+# The listed IP and address normally reported by a replica is obtained
+# in the following way:
+#
+# IP: The address is auto detected by checking the peer address
+# of the socket used by the replica to connect with the master.
+#
+# Port: The port is communicated by the replica during the replication
+# handshake, and is normally the port that the replica is using to
+# listen for connections.
+#
+# However when port forwarding or Network Address Translation (NAT) is
+# used, the replica may be actually reachable via different IP and port
+# pairs. The following two options can be used by a replica in order to
+# report to its master a specific set of IP and port, so that both INFO
+# and ROLE will report those values.
+#
+# There is no need to use both the options if you need to override just
+# the port or the IP address.
+#
+# replica-announce-ip 5.5.5.5
+# replica-announce-port 1234
+
+################################## SECURITY ###################################
+
+# Require clients to issue AUTH <PASSWORD> before processing any other
+# commands. This might be useful in environments in which you do not trust
+# others with access to the host running redis-server.
+#
+# This should stay commented out for backward compatibility and because most
+# people do not need auth (e.g. they run their own servers).
+#
+# Warning: since Redis is pretty fast an outside user can try up to
+# 150k passwords per second against a good box. This means that you should
+# use a very strong password otherwise it will be very easy to break.
+#
+# requirepass foobared
+
+# Command renaming.
+#
+# It is possible to change the name of dangerous commands in a shared
+# environment. For instance the CONFIG command may be renamed into something
+# hard to guess so that it will still be available for internal-use tools
+# but not available for general clients.
+#
+# Example:
+#
+# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
+#
+# It is also possible to completely kill a command by renaming it into
+# an empty string:
+#
+# rename-command CONFIG ""
+#
+# Please note that changing the name of commands that are logged into the
+# AOF file or transmitted to replicas may cause problems.
+
+################################### CLIENTS ####################################
+
+# Set the max number of connected clients at the same time. By default
+# this limit is set to 10000 clients, however if the Redis server is not
+# able to configure the process file limit to allow for the specified limit
+# the max number of allowed clients is set to the current file limit
+# minus 32 (as Redis reserves a few file descriptors for internal uses).
+#
+# Once the limit is reached Redis will close all the new connections sending
+# an error 'max number of clients reached'.
+#
+# maxclients 10000
+
+############################## MEMORY MANAGEMENT ################################
+
+# If Redis is to be used as an in-memory-only cache without any kind of
+# persistence, then the fork() mechanism used by the background AOF/RDB
+# persistence is unnecessary. As an optimization, all persistence can be
+# turned off in the Windows version of Redis. This will redirect heap
+# allocations to the system heap allocator, and disable commands that would
+# otherwise cause fork() operations: BGSAVE and BGREWRITEAOF.
+# This flag may not be combined with any of the other flags that configure
+# AOF and RDB operations.
+# persistence-available [(yes)|no]
+
+# Set a memory usage limit to the specified amount of bytes.
+# When the memory limit is reached Redis will try to remove keys
+# according to the eviction policy selected (see maxmemory-policy).
+#
+# If Redis can't remove keys according to the policy, or if the policy is
+# set to 'noeviction', Redis will start to reply with errors to commands
+# that would use more memory, like SET, LPUSH, and so on, and will continue
+# to reply to read-only commands like GET.
+#
+# This option is usually useful when using Redis as an LRU or LFU cache, or to
+# set a hard memory limit for an instance (using the 'noeviction' policy).
+#
+# WARNING: If you have replicas attached to an instance with maxmemory on,
+# the size of the output buffers needed to feed the replicas are subtracted
+# from the used memory count, so that network problems / resyncs will
+# not trigger a loop where keys are evicted, and in turn the output
+# buffer of replicas is full with DELs of keys evicted triggering the deletion
+# of more keys, and so forth until the database is completely emptied.
+#
+# In short... if you have replicas attached it is suggested that you set a lower
+# limit for maxmemory so that there is some free RAM on the system for replica
+# output buffers (but this is not needed if the policy is 'noeviction').
+#
+# WARNING: not setting maxmemory will cause Redis to terminate with an
+# out-of-memory exception if the heap limit is reached.
+#
+# NOTE: since Redis uses the system paging file to allocate the heap memory,
+# the Working Set memory usage showed by the Windows Task Manager or by other
+# tools such as ProcessExplorer will not always be accurate. For example, right
+# after a background save of the RDB or the AOF files, the working set value
+# may drop significantly. In order to check the correct amount of memory used
+# by the redis-server to store the data, use the INFO client command. The INFO
+# command shows only the memory used to store the redis data, not the extra
+# memory used by the Windows process for its own requirements. Th3 extra amount
+# of memory not reported by the INFO command can be calculated subtracting the
+# Peak Working Set reported by the Windows Task Manager and the used_memory_peak
+# reported by the INFO command.
+#
+# maxmemory <bytes>
+
+# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
+# is reached. You can select among five behaviors:
+#
+# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
+# allkeys-lru -> Evict any key using approximated LRU.
+# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
+# allkeys-lfu -> Evict any key using approximated LFU.
+# volatile-random -> Remove a random key among the ones with an expire set.
+# allkeys-random -> Remove a random key, any key.
+# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
+# noeviction -> Don't evict anything, just return an error on write operations.
+#
+# LRU means Least Recently Used
+# LFU means Least Frequently Used
+#
+# Both LRU, LFU and volatile-ttl are implemented using approximated
+# randomized algorithms.
+#
+# Note: with any of the above policies, Redis will return an error on write
+# operations, when there are no suitable keys for eviction.
+#
+# At the date of writing these commands are: set setnx setex append
+# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
+# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
+# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
+# getset mset msetnx exec sort
+#
+# The default is:
+#
+# maxmemory-policy noeviction
+
+# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
+# algorithms (in order to save memory), so you can tune it for speed or
+# accuracy. For default Redis will check five keys and pick the one that was
+# used less recently, you can change the sample size using the following
+# configuration directive.
+#
+# The default of 5 produces good enough results. 10 Approximates very closely
+# true LRU but costs more CPU. 3 is faster but not very accurate.
+#
+# maxmemory-samples 5
+
+# Starting from Redis 5, by default a replica will ignore its maxmemory setting
+# (unless it is promoted to master after a failover or manually). It means
+# that the eviction of keys will be just handled by the master, sending the
+# DEL commands to the replica as keys evict in the master side.
+#
+# This behavior ensures that masters and replicas stay consistent, and is usually
+# what you want, however if your replica is writable, or you want the replica to have
+# a different memory setting, and you are sure all the writes performed to the
+# replica are idempotent, then you may change this default (but be sure to understand
+# what you are doing).
+#
+# Note that since the replica by default does not evict, it may end using more
+# memory than the one set via maxmemory (there are certain buffers that may
+# be larger on the replica, or data structures may sometimes take more memory and so
+# forth). So make sure you monitor your replicas and make sure they have enough
+# memory to never hit a real out-of-memory condition before the master hits
+# the configured maxmemory setting.
+#
+# replica-ignore-maxmemory yes
+
+############################# LAZY FREEING ####################################
+
+# Redis has two primitives to delete keys. One is called DEL and is a blocking
+# deletion of the object. It means that the server stops processing new commands
+# in order to reclaim all the memory associated with an object in a synchronous
+# way. If the key deleted is associated with a small object, the time needed
+# in order to execute the DEL command is very small and comparable to most other
+# O(1) or O(log_N) commands in Redis. However if the key is associated with an
+# aggregated value containing millions of elements, the server can block for
+# a long time (even seconds) in order to complete the operation.
+#
+# For the above reasons Redis also offers non blocking deletion primitives
+# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and
+# FLUSHDB commands, in order to reclaim memory in background. Those commands
+# are executed in constant time. Another thread will incrementally free the
+# object in the background as fast as possible.
+#
+# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.
+# It's up to the design of the application to understand when it is a good
+# idea to use one or the other. However the Redis server sometimes has to
+# delete keys or flush the whole database as a side effect of other operations.
+# Specifically Redis deletes objects independently of a user call in the
+# following scenarios:
+#
+# 1) On eviction, because of the maxmemory and maxmemory policy configurations,
+# in order to make room for new data, without going over the specified
+# memory limit.
+# 2) Because of expire: when a key with an associated time to live (see the
+# EXPIRE command) must be deleted from memory.
+# 3) Because of a side effect of a command that stores data on a key that may
+# already exist. For example the RENAME command may delete the old key
+# content when it is replaced with another one. Similarly SUNIONSTORE
+# or SORT with STORE option may delete existing keys. The SET command
+# itself removes any old content of the specified key in order to replace
+# it with the specified string.
+# 4) During replication, when a replica performs a full resynchronization with
+# its master, the content of the whole database is removed in order to
+# load the RDB file just transferred.
+#
+# In all the above cases the default is to delete objects in a blocking way,
+# like if DEL was called. However you can configure each case specifically
+# in order to instead release memory in a non-blocking way like if UNLINK
+# was called, using the following configuration directives:
+
+lazyfree-lazy-eviction no
+lazyfree-lazy-expire no
+lazyfree-lazy-server-del no
+replica-lazy-flush no
+
+############################## APPEND ONLY MODE ###############################
+
+# By default Redis asynchronously dumps the dataset on disk. This mode is
+# good enough in many applications, but an issue with the Redis process or
+# a power outage may result into a few minutes of writes lost (depending on
+# the configured save points).
+#
+# The Append Only File is an alternative persistence mode that provides
+# much better durability. For instance using the default data fsync policy
+# (see later in the config file) Redis can lose just one second of writes in a
+# dramatic event like a server power outage, or a single write if something
+# wrong with the Redis process itself happens, but the operating system is
+# still running correctly.
+#
+# AOF and RDB persistence can be enabled at the same time without problems.
+# If the AOF is enabled on startup Redis will load the AOF, that is the file
+# with the better durability guarantees.
+#
+# Please check http://redis.io/topics/persistence for more information.
+
+appendonly no
+
+# The name of the append only file (default: "appendonly.aof")
+
+appendfilename "appendonly.aof"
+
+# The fsync() call tells the Operating System to actually write data on disk
+# instead of waiting for more data in the output buffer. Some OS will really flush
+# data on disk, some other OS will just try to do it ASAP.
+#
+# Redis supports three different modes:
+#
+# no: don't fsync, just let the OS flush the data when it wants. Faster.
+# always: fsync after every write to the append only log. Slow, Safest.
+# everysec: fsync only one time every second. Compromise.
+#
+# The default is "everysec", as that's usually the right compromise between
+# speed and data safety. It's up to you to understand if you can relax this to
+# "no" that will let the operating system flush the output buffer when
+# it wants, for better performances (but if you can live with the idea of
+# some data loss consider the default persistence mode that's snapshotting),
+# or on the contrary, use "always" that's very slow but a bit safer than
+# everysec.
+#
+# More details please check the following article:
+# http://antirez.com/post/redis-persistence-demystified.html
+#
+# If unsure, use "everysec".
+
+# appendfsync always
+appendfsync everysec
+# appendfsync no
+
+# When the AOF fsync policy is set to always or everysec, and a background
+# saving process (a background save or AOF log background rewriting) is
+# performing a lot of I/O against the disk, in some Linux configurations
+# Redis may block too long on the fsync() call. Note that there is no fix for
+# this currently, as even performing fsync in a different thread will block
+# our synchronous write(2) call.
+#
+# In order to mitigate this problem it's possible to use the following option
+# that will prevent fsync() from being called in the main process while a
+# BGSAVE or BGREWRITEAOF is in progress.
+#
+# This means that while another child is saving, the durability of Redis is
+# the same as "appendfsync none". In practical terms, this means that it is
+# possible to lose up to 30 seconds of log in the worst scenario (with the
+# default Linux settings).
+#
+# If you have latency problems turn this to "yes". Otherwise leave it as
+# "no" that is the safest pick from the point of view of durability.
+
+no-appendfsync-on-rewrite no
+
+# Automatic rewrite of the append only file.
+# Redis is able to automatically rewrite the log file implicitly calling
+# BGREWRITEAOF when the AOF log size grows by the specified percentage.
+#
+# This is how it works: Redis remembers the size of the AOF file after the
+# latest rewrite (if no rewrite has happened since the restart, the size of
+# the AOF at startup is used).
+#
+# This base size is compared to the current size. If the current size is
+# bigger than the specified percentage, the rewrite is triggered. Also
+# you need to specify a minimal size for the AOF file to be rewritten, this
+# is useful to avoid rewriting the AOF file even if the percentage increase
+# is reached but it is still pretty small.
+#
+# Specify a percentage of zero in order to disable the automatic AOF
+# rewrite feature.
+
+auto-aof-rewrite-percentage 100
+auto-aof-rewrite-min-size 64mb
+
+# An AOF file may be found to be truncated at the end during the Redis
+# startup process, when the AOF data gets loaded back into memory.
+# This may happen when the system where Redis is running
+# crashes, especially when an ext4 filesystem is mounted without the
+# data=ordered option (however this can't happen when Redis itself
+# crashes or aborts but the operating system still works correctly).
+#
+# Redis can either exit with an error when this happens, or load as much
+# data as possible (the default now) and start if the AOF file is found
+# to be truncated at the end. The following option controls this behavior.
+#
+# If aof-load-truncated is set to yes, a truncated AOF file is loaded and
+# the Redis server starts emitting a log to inform the user of the event.
+# Otherwise if the option is set to no, the server aborts with an error
+# and refuses to start. When the option is set to no, the user requires
+# to fix the AOF file using the "redis-check-aof" utility before to restart
+# the server.
+#
+# Note that if the AOF file will be found to be corrupted in the middle
+# the server will still exit with an error. This option only applies when
+# Redis will try to read more data from the AOF file but not enough bytes
+# will be found.
+aof-load-truncated yes
+
+# When rewriting the AOF file, Redis is able to use an RDB preamble in the
+# AOF file for faster rewrites and recoveries. When this option is turned
+# on the rewritten AOF file is composed of two different stanzas:
+#
+# [RDB file][AOF tail]
+#
+# When loading Redis recognizes that the AOF file starts with the "REDIS"
+# string and loads the prefixed RDB file, and continues loading the AOF
+# tail.
+aof-use-rdb-preamble yes
+
+################################ LUA SCRIPTING ###############################
+
+# Max execution time of a Lua script in milliseconds.
+#
+# If the maximum execution time is reached Redis will log that a script is
+# still in execution after the maximum allowed time and will start to
+# reply to queries with an error.
+#
+# When a long running script exceeds the maximum execution time only the
+# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
+# used to stop a script that did not yet called write commands. The second
+# is the only way to shut down the server in the case a write command was
+# already issued by the script but the user doesn't want to wait for the natural
+# termination of the script.
+#
+# Set it to 0 or a negative value for unlimited execution without warnings.
+lua-time-limit 5000
+
+################################ REDIS CLUSTER ###############################
+
+# Normal Redis instances can't be part of a Redis Cluster; only nodes that are
+# started as cluster nodes can. In order to start a Redis instance as a
+# cluster node enable the cluster support uncommenting the following:
+#
+# cluster-enabled yes
+
+# Every cluster node has a cluster configuration file. This file is not
+# intended to be edited by hand. It is created and updated by Redis nodes.
+# Every Redis Cluster node requires a different cluster configuration file.
+# Make sure that instances running in the same system do not have
+# overlapping cluster configuration file names.
+#
+# cluster-config-file nodes-6379.conf
+
+# Cluster node timeout is the amount of milliseconds a node must be unreachable
+# for it to be considered in failure state.
+# Most other internal time limits are multiple of the node timeout.
+#
+# cluster-node-timeout 15000
+
+# A replica of a failing master will avoid to start a failover if its data
+# looks too old.
+#
+# There is no simple way for a replica to actually have an exact measure of
+# its "data age", so the following two checks are performed:
+#
+# 1) If there are multiple replicas able to failover, they exchange messages
+# in order to try to give an advantage to the replica with the best
+# replication offset (more data from the master processed).
+# Replicas will try to get their rank by offset, and apply to the start
+# of the failover a delay proportional to their rank.
+#
+# 2) Every single replica computes the time of the last interaction with
+# its master. This can be the last ping or command received (if the master
+# is still in the "connected" state), or the time that elapsed since the
+# disconnection with the master (if the replication link is currently down).
+# If the last interaction is too old, the replica will not try to failover
+# at all.
+#
+# The point "2" can be tuned by user. Specifically a replica will not perform
+# the failover if, since the last interaction with the master, the time
+# elapsed is greater than:
+#
+# (node-timeout * replica-validity-factor) + repl-ping-replica-period
+#
+# So for example if node-timeout is 30 seconds, and the replica-validity-factor
+# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the
+# replica will not try to failover if it was not able to talk with the master
+# for longer than 310 seconds.
+#
+# A large replica-validity-factor may allow replicas with too old data to failover
+# a master, while a too small value may prevent the cluster from being able to
+# elect a replica at all.
+#
+# For maximum availability, it is possible to set the replica-validity-factor
+# to a value of 0, which means, that replicas will always try to failover the
+# master regardless of the last time they interacted with the master.
+# (However they'll always try to apply a delay proportional to their
+# offset rank).
+#
+# Zero is the only value able to guarantee that when all the partitions heal
+# the cluster will always be able to continue.
+#
+# cluster-replica-validity-factor 10
+
+# Cluster replicas are able to migrate to orphaned masters, that are masters
+# that are left without working replicas. This improves the cluster ability
+# to resist to failures as otherwise an orphaned master can't be failed over
+# in case of failure if it has no working replicas.
+#
+# Replicas migrate to orphaned masters only if there are still at least a
+# given number of other working replicas for their old master. This number
+# is the "migration barrier". A migration barrier of 1 means that a replica
+# will migrate only if there is at least 1 other working replica for its master
+# and so forth. It usually reflects the number of replicas you want for every
+# master in your cluster.
+#
+# Default is 1 (replicas migrate only if their masters remain with at least
+# one replica). To disable migration just set it to a very large value.
+# A value of 0 can be set but is useful only for debugging and dangerous
+# in production.
+#
+# cluster-migration-barrier 1
+
+# By default Redis Cluster nodes stop accepting queries if they detect there
+# is at least an hash slot uncovered (no available node is serving it).
+# This way if the cluster is partially down (for example a range of hash slots
+# are no longer covered) all the cluster becomes, eventually, unavailable.
+# It automatically returns available as soon as all the slots are covered again.
+#
+# However sometimes you want the subset of the cluster which is working,
+# to continue to accept queries for the part of the key space that is still
+# covered. In order to do so, just set the cluster-require-full-coverage
+# option to no.
+#
+# cluster-require-full-coverage yes
+
+# This option, when set to yes, prevents replicas from trying to failover its
+# master during master failures. However the master can still perform a
+# manual failover, if forced to do so.
+#
+# This is useful in different scenarios, especially in the case of multiple
+# data center operations, where we want one side to never be promoted if not
+# in the case of a total DC failure.
+#
+# cluster-replica-no-failover no
+
+# In order to setup your cluster make sure to read the documentation
+# available at http://redis.io web site.
+
+########################## CLUSTER DOCKER/NAT support ########################
+
+# In certain deployments, Redis Cluster nodes address discovery fails, because
+# addresses are NAT-ted or because ports are forwarded (the typical case is
+# Docker and other containers).
+#
+# In order to make Redis Cluster working in such environments, a static
+# configuration where each node knows its public address is needed. The
+# following two options are used for this scope, and are:
+#
+# * cluster-announce-ip
+# * cluster-announce-port
+# * cluster-announce-bus-port
+#
+# Each instruct the node about its address, client port, and cluster message
+# bus port. The information is then published in the header of the bus packets
+# so that other nodes will be able to correctly map the address of the node
+# publishing the information.
+#
+# If the above options are not used, the normal Redis Cluster auto-detection
+# will be used instead.
+#
+# Note that when remapped, the bus port may not be at the fixed offset of
+# clients port + 10000, so you can specify any port and bus-port depending
+# on how they get remapped. If the bus-port is not set, a fixed offset of
+# 10000 will be used as usually.
+#
+# Example:
+#
+# cluster-announce-ip 10.1.1.5
+# cluster-announce-port 6379
+# cluster-announce-bus-port 6380
+
+################################## SLOW LOG ###################################
+
+# The Redis Slow Log is a system to log queries that exceeded a specified
+# execution time. The execution time does not include the I/O operations
+# like talking with the client, sending the reply and so forth,
+# but just the time needed to actually execute the command (this is the only
+# stage of command execution where the thread is blocked and can not serve
+# other requests in the meantime).
+#
+# You can configure the slow log with two parameters: one tells Redis
+# what is the execution time, in microseconds, to exceed in order for the
+# command to get logged, and the other parameter is the length of the
+# slow log. When a new command is logged the oldest one is removed from the
+# queue of logged commands.
+
+# The following time is expressed in microseconds, so 1000000 is equivalent
+# to one second. Note that a negative number disables the slow log, while
+# a value of zero forces the logging of every command.
+slowlog-log-slower-than 10000
+
+# There is no limit to this length. Just be aware that it will consume memory.
+# You can reclaim memory used by the slow log with SLOWLOG RESET.
+slowlog-max-len 128
+
+################################ LATENCY MONITOR ##############################
+
+# The Redis latency monitoring subsystem samples different operations
+# at runtime in order to collect data related to possible sources of
+# latency of a Redis instance.
+#
+# Via the LATENCY command this information is available to the user that can
+# print graphs and obtain reports.
+#
+# The system only logs operations that were performed in a time equal or
+# greater than the amount of milliseconds specified via the
+# latency-monitor-threshold configuration directive. When its value is set
+# to zero, the latency monitor is turned off.
+#
+# By default latency monitoring is disabled since it is mostly not needed
+# if you don't have latency issues, and collecting data has a performance
+# impact, that while very small, can be measured under big load. Latency
+# monitoring can easily be enabled at runtime using the command
+# "CONFIG SET latency-monitor-threshold <milliseconds>" if needed.
+latency-monitor-threshold 0
+
+############################# EVENT NOTIFICATION ##############################
+
+# Redis can notify Pub/Sub clients about events happening in the key space.
+# This feature is documented at http://redis.io/topics/notifications
+#
+# For instance if keyspace events notification is enabled, and a client
+# performs a DEL operation on key "foo" stored in the Database 0, two
+# messages will be published via Pub/Sub:
+#
+# PUBLISH __keyspace@0__:foo del
+# PUBLISH __keyevent@0__:del foo
+#
+# It is possible to select the events that Redis will notify among a set
+# of classes. Every class is identified by a single character:
+#
+# K Keyspace events, published with __keyspace@<db>__ prefix.
+# E Keyevent events, published with __keyevent@<db>__ prefix.
+# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
+# $ String commands
+# l List commands
+# s Set commands
+# h Hash commands
+# z Sorted set commands
+# x Expired events (events generated every time a key expires)
+# e Evicted events (events generated when a key is evicted for maxmemory)
+# A Alias for g$lshzxe, so that the "AKE" string means all the events.
+#
+# The "notify-keyspace-events" takes as argument a string that is composed
+# of zero or multiple characters. The empty string means that notifications
+# are disabled.
+#
+# Example: to enable list and generic events, from the point of view of the
+# event name, use:
+#
+# notify-keyspace-events Elg
+#
+# Example 2: to get the stream of the expired keys subscribing to channel
+# name __keyevent@0__:expired use:
+#
+# notify-keyspace-events Ex
+#
+# By default all notifications are disabled because most users don't need
+# this feature and the feature has some overhead. Note that if you don't
+# specify at least one of K or E, no events will be delivered.
+notify-keyspace-events ""
+
+############################### ADVANCED CONFIG ###############################
+
+# Hashes are encoded using a memory efficient data structure when they have a
+# small number of entries, and the biggest entry does not exceed a given
+# threshold. These thresholds can be configured using the following directives.
+hash-max-ziplist-entries 512
+hash-max-ziplist-value 64
+
+# Lists are also encoded in a special way to save a lot of space.
+# The number of entries allowed per internal list node can be specified
+# as a fixed maximum size or a maximum number of elements.
+# For a fixed maximum size, use -5 through -1, meaning:
+# -5: max size: 64 Kb <-- not recommended for normal workloads
+# -4: max size: 32 Kb <-- not recommended
+# -3: max size: 16 Kb <-- probably not recommended
+# -2: max size: 8 Kb <-- good
+# -1: max size: 4 Kb <-- good
+# Positive numbers mean store up to _exactly_ that number of elements
+# per list node.
+# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
+# but if your use case is unique, adjust the settings as necessary.
+list-max-ziplist-size -2
+
+# Lists may also be compressed.
+# Compress depth is the number of quicklist ziplist nodes from *each* side of
+# the list to *exclude* from compression. The head and tail of the list
+# are always uncompressed for fast push/pop operations. Settings are:
+# 0: disable all list compression
+# 1: depth 1 means "don't start compressing until after 1 node into the list,
+# going from either the head or tail"
+# So: [head]->node->node->...->node->[tail]
+# [head], [tail] will always be uncompressed; inner nodes will compress.
+# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
+# 2 here means: don't compress head or head->next or tail->prev or tail,
+# but compress all nodes between them.
+# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
+# etc.
+list-compress-depth 0
+
+# Sets have a special encoding in just one case: when a set is composed
+# of just strings that happen to be integers in radix 10 in the range
+# of 64 bit signed integers.
+# The following configuration setting sets the limit in the size of the
+# set in order to use this special memory saving encoding.
+set-max-intset-entries 512
+
+# Similarly to hashes and lists, sorted sets are also specially encoded in
+# order to save a lot of space. This encoding is only used when the length and
+# elements of a sorted set are below the following limits:
+zset-max-ziplist-entries 128
+zset-max-ziplist-value 64
+
+# HyperLogLog sparse representation bytes limit. The limit includes the
+# 16 bytes header. When an HyperLogLog using the sparse representation crosses
+# this limit, it is converted into the dense representation.
+#
+# A value greater than 16000 is totally useless, since at that point the
+# dense representation is more memory efficient.
+#
+# The suggested value is ~ 3000 in order to have the benefits of
+# the space efficient encoding without slowing down too much PFADD,
+# which is O(N) with the sparse encoding. The value can be raised to
+# ~ 10000 when CPU is not a concern, but space is, and the data set is
+# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.
+hll-sparse-max-bytes 3000
+
+# Streams macro node max size / items. The stream data structure is a radix
+# tree of big nodes that encode multiple items inside. Using this configuration
+# it is possible to configure how big a single node can be in bytes, and the
+# maximum number of items it may contain before switching to a new node when
+# appending new stream entries. If any of the following settings are set to
+# zero, the limit is ignored, so for instance it is possible to set just a
+# max entires limit by setting max-bytes to 0 and max-entries to the desired
+# value.
+stream-node-max-bytes 4096
+stream-node-max-entries 100
+
+# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in
+# order to help rehashing the main Redis hash table (the one mapping top-level
+# keys to values). The hash table implementation Redis uses (see dict.c)
+# performs a lazy rehashing: the more operation you run into a hash table
+# that is rehashing, the more rehashing "steps" are performed, so if the
+# server is idle the rehashing is never complete and some more memory is used
+# by the hash table.
+#
+# The default is to use this millisecond 10 times every second in order to
+# actively rehash the main dictionaries, freeing memory when possible.
+#
+# If unsure:
+# use "activerehashing no" if you have hard latency requirements and it is
+# not a good thing in your environment that Redis can reply from time to time
+# to queries with 2 milliseconds delay.
+#
+# use "activerehashing yes" if you don't have such hard requirements but
+# want to free memory asap when possible.
+activerehashing yes
+
+# The client output buffer limits can be used to force disconnection of clients
+# that are not reading data from the server fast enough for some reason (a
+# common reason is that a Pub/Sub client can't consume messages as fast as the
+# publisher can produce them).
+#
+# The limit can be set differently for the three different classes of clients:
+#
+# normal -> normal clients including MONITOR clients
+# replica -> replica clients
+# pubsub -> clients subscribed to at least one pubsub channel or pattern
+#
+# The syntax of every client-output-buffer-limit directive is the following:
+#
+# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
+#
+# A client is immediately disconnected once the hard limit is reached, or if
+# the soft limit is reached and remains reached for the specified number of
+# seconds (continuously).
+# So for instance if the hard limit is 32 megabytes and the soft limit is
+# 16 megabytes / 10 seconds, the client will get disconnected immediately
+# if the size of the output buffers reach 32 megabytes, but will also get
+# disconnected if the client reaches 16 megabytes and continuously overcomes
+# the limit for 10 seconds.
+#
+# By default normal clients are not limited because they don't receive data
+# without asking (in a push way), but just after a request, so only
+# asynchronous clients may create a scenario where data is requested faster
+# than it can read.
+#
+# Instead there is a default limit for pubsub and replica clients, since
+# subscribers and replicas receive data in a push fashion.
+#
+# Both the hard or the soft limit can be disabled by setting them to zero.
+client-output-buffer-limit normal 0 0 0
+client-output-buffer-limit replica 256mb 64mb 60
+client-output-buffer-limit pubsub 32mb 8mb 60
+
+# Client query buffers accumulate new commands. They are limited to a fixed
+# amount by default in order to avoid that a protocol desynchronization (for
+# instance due to a bug in the client) will lead to unbound memory usage in
+# the query buffer. However you can configure it here if you have very special
+# needs, such us huge multi/exec requests or alike.
+#
+# client-query-buffer-limit 1gb
+
+# In the Redis protocol, bulk requests, that are, elements representing single
+# strings, are normally limited ot 512 mb. However you can change this limit
+# here.
+#
+# proto-max-bulk-len 512mb
+
+# Redis calls an internal function to perform many background tasks, like
+# closing connections of clients in timeout, purging expired keys that are
+# never requested, and so forth.
+#
+# Not all tasks are performed with the same frequency, but Redis checks for
+# tasks to perform according to the specified "hz" value.
+#
+# By default "hz" is set to 10. Raising the value will use more CPU when
+# Redis is idle, but at the same time will make Redis more responsive when
+# there are many keys expiring at the same time, and timeouts may be
+# handled with more precision.
+#
+# The range is between 1 and 500, however a value over 100 is usually not
+# a good idea. Most users should use the default of 10 and raise this up to
+# 100 only in environments where very low latency is required.
+hz 10
+
+# Normally it is useful to have an HZ value which is proportional to the
+# number of clients connected. This is useful in order, for instance, to
+# avoid too many clients are processed for each background task invocation
+# in order to avoid latency spikes.
+#
+# Since the default HZ value by default is conservatively set to 10, Redis
+# offers, and enables by default, the ability to use an adaptive HZ value
+# which will temporary raise when there are many connected clients.
+#
+# When dynamic HZ is enabled, the actual configured HZ will be used as
+# as a baseline, but multiples of the configured HZ value will be actually
+# used as needed once more clients are connected. In this way an idle
+# instance will use very little CPU time while a busy instance will be
+# more responsive.
+dynamic-hz yes
+
+# When a child rewrites the AOF file, if the following option is enabled
+# the file will be fsync-ed every 32 MB of data generated. This is useful
+# in order to commit the file to the disk more incrementally and avoid
+# big latency spikes.
+aof-rewrite-incremental-fsync yes
+
+# When redis saves RDB file, if the following option is enabled
+# the file will be fsync-ed every 32 MB of data generated. This is useful
+# in order to commit the file to the disk more incrementally and avoid
+# big latency spikes.
+rdb-save-incremental-fsync yes
+
+# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good
+# idea to start with the default settings and only change them after investigating
+# how to improve the performances and how the keys LFU change over time, which
+# is possible to inspect via the OBJECT FREQ command.
+#
+# There are two tunable parameters in the Redis LFU implementation: the
+# counter logarithm factor and the counter decay time. It is important to
+# understand what the two parameters mean before changing them.
+#
+# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis
+# uses a probabilistic increment with logarithmic behavior. Given the value
+# of the old counter, when a key is accessed, the counter is incremented in
+# this way:
+#
+# 1. A random number R between 0 and 1 is extracted.
+# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).
+# 3. The counter is incremented only if R < P.
+#
+# The default lfu-log-factor is 10. This is a table of how the frequency
+# counter changes with a different number of accesses with different
+# logarithmic factors:
+#
+# +--------+------------+------------+------------+------------+------------+
+# | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
+# +--------+------------+------------+------------+------------+------------+
+# | 0 | 104 | 255 | 255 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 1 | 18 | 49 | 255 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 10 | 10 | 18 | 142 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 100 | 8 | 11 | 49 | 143 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+#
+# NOTE: The above table was obtained by running the following commands:
+#
+# redis-benchmark -n 1000000 incr foo
+# redis-cli object freq foo
+#
+# NOTE 2: The counter initial value is 5 in order to give new objects a chance
+# to accumulate hits.
+#
+# The counter decay time is the time, in minutes, that must elapse in order
+# for the key counter to be divided by two (or decremented if it has a value
+# less <= 10).
+#
+# The default value for the lfu-decay-time is 1. A Special value of 0 means to
+# decay the counter every time it happens to be scanned.
+#
+# lfu-log-factor 10
+# lfu-decay-time 1
+
+################################## INCLUDES ###################################
+
+# Include one or more other config files here. This is useful if you
+# have a standard template that goes to all Redis server but also need
+# to customize a few per-server settings. Include files can include
+# other files, so use this wisely.
+#
+# include /path/to/local.conf
+# include /path/to/other.conf
diff --git a/Redis-x64-5.0.14.1/redis.windows.conf b/Redis-x64-5.0.14.1/redis.windows.conf
new file mode 100644
index 0000000..8233249
--- /dev/null
+++ b/Redis-x64-5.0.14.1/redis.windows.conf
@@ -0,0 +1,1336 @@
+# Redis configuration file example
+
+# Note on units: when memory size is needed, it is possible to specify
+# it in the usual form of 1k 5GB 4M and so forth:
+#
+# 1k => 1000 bytes
+# 1kb => 1024 bytes
+# 1m => 1000000 bytes
+# 1mb => 1024*1024 bytes
+# 1g => 1000000000 bytes
+# 1gb => 1024*1024*1024 bytes
+#
+# units are case insensitive so 1GB 1Gb 1gB are all the same.
+
+################################## INCLUDES ###################################
+
+# Include one or more other config files here. This is useful if you
+# have a standard template that goes to all Redis servers but also need
+# to customize a few per-server settings. Include files can include
+# other files, so use this wisely.
+#
+# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
+# from admin or Redis Sentinel. Since Redis always uses the last processed
+# line as value of a configuration directive, you'd better put includes
+# at the beginning of this file to avoid overwriting config change at runtime.
+#
+# If instead you are interested in using includes to override configuration
+# options, it is better to use include as the last line.
+#
+# include .\path\to\local.conf
+# include c:\path\to\other.conf
+
+################################## MODULES #####################################
+
+# Load modules at startup. If the server is not able to load modules
+# it will abort. It is possible to use multiple loadmodule directives.
+#
+# loadmodule .\path\to\my_module.dll
+# loadmodule c:\path\to\other_module.dll
+
+################################## NETWORK #####################################
+
+# By default, if no "bind" configuration directive is specified, Redis listens
+# for connections from all the network interfaces available on the server.
+# It is possible to listen to just one or multiple selected interfaces using
+# the "bind" configuration directive, followed by one or more IP addresses.
+#
+# Examples:
+#
+# bind 192.168.1.100 10.0.0.1
+# bind 127.0.0.1 ::1
+#
+# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
+# internet, binding to all the interfaces is dangerous and will expose the
+# instance to everybody on the internet. So by default we uncomment the
+# following bind directive, that will force Redis to listen only into
+# the IPv4 loopback interface address (this means Redis will be able to
+# accept connections only from clients running into the same computer it
+# is running).
+#
+# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
+# JUST COMMENT THE FOLLOWING LINE.
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+bind 127.0.0.1
+
+# Protected mode is a layer of security protection, in order to avoid that
+# Redis instances left open on the internet are accessed and exploited.
+#
+# When protected mode is on and if:
+#
+# 1) The server is not binding explicitly to a set of addresses using the
+# "bind" directive.
+# 2) No password is configured.
+#
+# The server only accepts connections from clients connecting from the
+# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
+# sockets.
+#
+# By default protected mode is enabled. You should disable it only if
+# you are sure you want clients from other hosts to connect to Redis
+# even if no authentication is configured, nor a specific set of interfaces
+# are explicitly listed using the "bind" directive.
+protected-mode yes
+
+# Accept connections on the specified port, default is 6379 (IANA #815344).
+# If port 0 is specified Redis will not listen on a TCP socket.
+port 6379
+
+# TCP listen() backlog.
+#
+# In high requests-per-second environments you need an high backlog in order
+# to avoid slow clients connections issues. Note that the Linux kernel
+# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
+# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
+# in order to get the desired effect.
+tcp-backlog 511
+
+# Unix socket.
+#
+# Specify the path for the Unix socket that will be used to listen for
+# incoming connections. There is no default, so Redis will not listen
+# on a unix socket when not specified.
+#
+# unixsocket /tmp/redis.sock
+# unixsocketperm 700
+
+# Close the connection after a client is idle for N seconds (0 to disable)
+timeout 0
+
+# TCP keepalive.
+#
+# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
+# of communication. This is useful for two reasons:
+#
+# 1) Detect dead peers.
+# 2) Take the connection alive from the point of view of network
+# equipment in the middle.
+#
+# On Linux, the specified value (in seconds) is the period used to send ACKs.
+# Note that to close the connection the double of the time is needed.
+# On other kernels the period depends on the kernel configuration.
+#
+# A reasonable value for this option is 300 seconds, which is the new
+# Redis default starting with Redis 3.2.1.
+tcp-keepalive 300
+
+################################# GENERAL #####################################
+
+# By default Redis does not run as a daemon. Use 'yes' if you need it.
+# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
+# NOT SUPPORTED ON WINDOWS daemonize no
+
+# If you run Redis from upstart or systemd, Redis can interact with your
+# supervision tree. Options:
+# supervised no - no supervision interaction
+# supervised upstart - signal upstart by putting Redis into SIGSTOP mode
+# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
+# supervised auto - detect upstart or systemd method based on
+# UPSTART_JOB or NOTIFY_SOCKET environment variables
+# Note: these supervision methods only signal "process is ready."
+# They do not enable continuous liveness pings back to your supervisor.
+# NOT SUPPORTED ON WINDOWS supervised no
+
+# If a pid file is specified, Redis writes it where specified at startup
+# and removes it at exit.
+#
+# When the server runs non daemonized, no pid file is created if none is
+# specified in the configuration. When the server is daemonized, the pid file
+# is used even if not specified, defaulting to "/var/run/redis.pid".
+#
+# Creating a pid file is best effort: if Redis is not able to create it
+# nothing bad happens, the server will start and run normally.
+# NOT SUPPORTED ON WINDOWS pidfile /var/run/redis.pid
+
+# Specify the server verbosity level.
+# This can be one of:
+# debug (a lot of information, useful for development/testing)
+# verbose (many rarely useful info, but not a mess like the debug level)
+# notice (moderately verbose, what you want in production probably)
+# warning (only very important / critical messages are logged)
+loglevel notice
+
+# Specify the log file name. Also 'stdout' can be used to force
+# Redis to log on the standard output.
+logfile ""
+
+# To enable logging to the Windows EventLog, just set 'syslog-enabled' to
+# yes, and optionally update the other syslog parameters to suit your needs.
+# If Redis is installed and launched as a Windows Service, this will
+# automatically be enabled.
+# syslog-enabled no
+
+# Specify the source name of the events in the Windows Application log.
+# syslog-ident redis
+
+# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
+# NOT SUPPORTED ON WINDOWS syslog-facility local0
+
+# Set the number of databases. The default database is DB 0, you can select
+# a different one on a per-connection basis using SELECT <dbid> where
+# dbid is a number between 0 and 'databases'-1
+databases 16
+
+# By default Redis shows an ASCII art logo only when started to log to the
+# standard output and if the standard output is a TTY. Basically this means
+# that normally a logo is displayed only in interactive sessions.
+#
+# However it is possible to force the pre-4.0 behavior and always show a
+# ASCII art logo in startup logs by setting the following option to yes.
+always-show-logo yes
+
+################################ SNAPSHOTTING ################################
+#
+# Save the DB on disk:
+#
+# save <seconds> <changes>
+#
+# Will save the DB if both the given number of seconds and the given
+# number of write operations against the DB occurred.
+#
+# In the example below the behaviour will be to save:
+# after 900 sec (15 min) if at least 1 key changed
+# after 300 sec (5 min) if at least 10 keys changed
+# after 60 sec if at least 10000 keys changed
+#
+# Note: you can disable saving completely by commenting out all "save" lines.
+#
+# It is also possible to remove all the previously configured save
+# points by adding a save directive with a single empty string argument
+# like in the following example:
+#
+# save ""
+
+save 900 1
+save 300 10
+save 60 10000
+
+# By default Redis will stop accepting writes if RDB snapshots are enabled
+# (at least one save point) and the latest background save failed.
+# This will make the user aware (in a hard way) that data is not persisting
+# on disk properly, otherwise chances are that no one will notice and some
+# disaster will happen.
+#
+# If the background saving process will start working again Redis will
+# automatically allow writes again.
+#
+# However if you have setup your proper monitoring of the Redis server
+# and persistence, you may want to disable this feature so that Redis will
+# continue to work as usual even if there are problems with disk,
+# permissions, and so forth.
+stop-writes-on-bgsave-error yes
+
+# Compress string objects using LZF when dump .rdb databases?
+# For default that's set to 'yes' as it's almost always a win.
+# If you want to save some CPU in the saving child set it to 'no' but
+# the dataset will likely be bigger if you have compressible values or keys.
+rdbcompression yes
+
+# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
+# This makes the format more resistant to corruption but there is a performance
+# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
+# for maximum performances.
+#
+# RDB files created with checksum disabled have a checksum of zero that will
+# tell the loading code to skip the check.
+rdbchecksum yes
+
+# The filename where to dump the DB
+dbfilename dump.rdb
+
+# The working directory.
+#
+# The DB will be written inside this directory, with the filename specified
+# above using the 'dbfilename' configuration directive.
+#
+# The Append Only File will also be created inside this directory.
+#
+# Note that you must specify a directory here, not a file name.
+dir ./
+
+################################# REPLICATION #################################
+
+# Master-Replica replication. Use replicaof to make a Redis instance a copy of
+# another Redis server. A few things to understand ASAP about Redis replication.
+#
+# +------------------+ +---------------+
+# | Master | ---> | Replica |
+# | (receive writes) | | (exact copy) |
+# +------------------+ +---------------+
+#
+# 1) Redis replication is asynchronous, but you can configure a master to
+# stop accepting writes if it appears to be not connected with at least
+# a given number of replicas.
+# 2) Redis replicas are able to perform a partial resynchronization with the
+# master if the replication link is lost for a relatively small amount of
+# time. You may want to configure the replication backlog size (see the next
+# sections of this file) with a sensible value depending on your needs.
+# 3) Replication is automatic and does not need user intervention. After a
+# network partition replicas automatically try to reconnect to masters
+# and resynchronize with them.
+#
+# replicaof <masterip> <masterport>
+
+# If the master is password protected (using the "requirepass" configuration
+# directive below) it is possible to tell the replica to authenticate before
+# starting the replication synchronization process, otherwise the master will
+# refuse the replica request.
+#
+# masterauth <master-password>
+
+# When a replica loses its connection with the master, or when the replication
+# is still in progress, the replica can act in two different ways:
+#
+# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will
+# still reply to client requests, possibly with out of date data, or the
+# data set may just be empty if this is the first synchronization.
+#
+# 2) if replica-serve-stale-data is set to 'no' the replica will reply with
+# an error "SYNC with master in progress" to all the kind of commands
+# but to INFO, replicaOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG,
+# SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB,
+# COMMAND, POST, HOST: and LATENCY.
+#
+replica-serve-stale-data yes
+
+# You can configure a replica instance to accept writes or not. Writing against
+# a replica instance may be useful to store some ephemeral data (because data
+# written on a replica will be easily deleted after resync with the master) but
+# may also cause problems if clients are writing to it because of a
+# misconfiguration.
+#
+# Since Redis 2.6 by default replicas are read-only.
+#
+# Note: read only replicas are not designed to be exposed to untrusted clients
+# on the internet. It's just a protection layer against misuse of the instance.
+# Still a read only replica exports by default all the administrative commands
+# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
+# security of read only replicas using 'rename-command' to shadow all the
+# administrative / dangerous commands.
+replica-read-only yes
+
+# Replication SYNC strategy: disk or socket.
+#
+# -------------------------------------------------------
+# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY
+# -------------------------------------------------------
+#
+# New replicas and reconnecting replicas that are not able to continue the replication
+# process just receiving differences, need to do what is called a "full
+# synchronization". An RDB file is transmitted from the master to the replicas.
+# The transmission can happen in two different ways:
+#
+# 1) Disk-backed: The Redis master creates a new process that writes the RDB
+# file on disk. Later the file is transferred by the parent
+# process to the replicas incrementally.
+# 2) Diskless: The Redis master creates a new process that directly writes the
+# RDB file to replica sockets, without touching the disk at all.
+#
+# With disk-backed replication, while the RDB file is generated, more replicas
+# can be queued and served with the RDB file as soon as the current child producing
+# the RDB file finishes its work. With diskless replication instead once
+# the transfer starts, new replicas arriving will be queued and a new transfer
+# will start when the current one terminates.
+#
+# When diskless replication is used, the master waits a configurable amount of
+# time (in seconds) before starting the transfer in the hope that multiple replicas
+# will arrive and the transfer can be parallelized.
+#
+# With slow disks and fast (large bandwidth) networks, diskless replication
+# works better.
+repl-diskless-sync no
+
+# When diskless replication is enabled, it is possible to configure the delay
+# the server waits in order to spawn the child that transfers the RDB via socket
+# to the replicas.
+#
+# This is important since once the transfer starts, it is not possible to serve
+# new replicas arriving, that will be queued for the next RDB transfer, so the server
+# waits a delay in order to let more replicas arrive.
+#
+# The delay is specified in seconds, and by default is 5 seconds. To disable
+# it entirely just set it to 0 seconds and the transfer will start ASAP.
+repl-diskless-sync-delay 5
+
+# Replicas send PINGs to server in a predefined interval. It's possible to change
+# this interval with the repl_ping_replica_period option. The default value is 10
+# seconds.
+#
+# repl-ping-replica-period 10
+
+# The following option sets the replication timeout for:
+#
+# 1) Bulk transfer I/O during SYNC, from the point of view of replica.
+# 2) Master timeout from the point of view of replicas (data, pings).
+# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).
+#
+# It is important to make sure that this value is greater than the value
+# specified for repl-ping-replica-period otherwise a timeout will be detected
+# every time there is low traffic between the master and the replica.
+#
+# repl-timeout 60
+
+# Disable TCP_NODELAY on the replica socket after SYNC?
+#
+# If you select "yes" Redis will use a smaller number of TCP packets and
+# less bandwidth to send data to replicas. But this can add a delay for
+# the data to appear on the replica side, up to 40 milliseconds with
+# Linux kernels using a default configuration.
+#
+# If you select "no" the delay for data to appear on the replica side will
+# be reduced but more bandwidth will be used for replication.
+#
+# By default we optimize for low latency, but in very high traffic conditions
+# or when the master and replicas are many hops away, turning this to "yes" may
+# be a good idea.
+repl-disable-tcp-nodelay no
+
+# Set the replication backlog size. The backlog is a buffer that accumulates
+# replica data when replicas are disconnected for some time, so that when a replica
+# wants to reconnect again, often a full resync is not needed, but a partial
+# resync is enough, just passing the portion of data the replica missed while
+# disconnected.
+#
+# The bigger the replication backlog, the longer the time the replica can be
+# disconnected and later be able to perform a partial resynchronization.
+#
+# The backlog is only allocated once there is at least a replica connected.
+#
+# repl-backlog-size 1mb
+
+# After a master has no longer connected replicas for some time, the backlog
+# will be freed. The following option configures the amount of seconds that
+# need to elapse, starting from the time the last replica disconnected, for
+# the backlog buffer to be freed.
+#
+# Note that replicas never free the backlog for timeout, since they may be
+# promoted to masters later, and should be able to correctly "partially
+# resynchronize" with the replicas: hence they should always accumulate backlog.
+#
+# A value of 0 means to never release the backlog.
+#
+# repl-backlog-ttl 3600
+
+# The replica priority is an integer number published by Redis in the INFO output.
+# It is used by Redis Sentinel in order to select a replica to promote into a
+# master if the master is no longer working correctly.
+#
+# A replica with a low priority number is considered better for promotion, so
+# for instance if there are three replicas with priority 10, 100, 25 Sentinel will
+# pick the one with priority 10, that is the lowest.
+#
+# However a special priority of 0 marks the replica as not able to perform the
+# role of master, so a replica with priority of 0 will never be selected by
+# Redis Sentinel for promotion.
+#
+# By default the priority is 100.
+replica-priority 100
+
+# It is possible for a master to stop accepting writes if there are less than
+# N replicas connected, having a lag less or equal than M seconds.
+#
+# The N replicas need to be in "online" state.
+#
+# The lag in seconds, that must be <= the specified value, is calculated from
+# the last ping received from the replica, that is usually sent every second.
+#
+# This option does not GUARANTEE that N replicas will accept the write, but
+# will limit the window of exposure for lost writes in case not enough replicas
+# are available, to the specified number of seconds.
+#
+# For example to require at least 3 replicas with a lag <= 10 seconds use:
+#
+# min-replicas-to-write 3
+# min-replicas-max-lag 10
+#
+# Setting one or the other to 0 disables the feature.
+#
+# By default min-replicas-to-write is set to 0 (feature disabled) and
+# min-replicas-max-lag is set to 10.
+
+# A Redis master is able to list the address and port of the attached
+# replicas in different ways. For example the "INFO replication" section
+# offers this information, which is used, among other tools, by
+# Redis Sentinel in order to discover replica instances.
+# Another place where this info is available is in the output of the
+# "ROLE" command of a master.
+#
+# The listed IP and address normally reported by a replica is obtained
+# in the following way:
+#
+# IP: The address is auto detected by checking the peer address
+# of the socket used by the replica to connect with the master.
+#
+# Port: The port is communicated by the replica during the replication
+# handshake, and is normally the port that the replica is using to
+# listen for connections.
+#
+# However when port forwarding or Network Address Translation (NAT) is
+# used, the replica may be actually reachable via different IP and port
+# pairs. The following two options can be used by a replica in order to
+# report to its master a specific set of IP and port, so that both INFO
+# and ROLE will report those values.
+#
+# There is no need to use both the options if you need to override just
+# the port or the IP address.
+#
+# replica-announce-ip 5.5.5.5
+# replica-announce-port 1234
+
+################################## SECURITY ###################################
+
+# Require clients to issue AUTH <PASSWORD> before processing any other
+# commands. This might be useful in environments in which you do not trust
+# others with access to the host running redis-server.
+#
+# This should stay commented out for backward compatibility and because most
+# people do not need auth (e.g. they run their own servers).
+#
+# Warning: since Redis is pretty fast an outside user can try up to
+# 150k passwords per second against a good box. This means that you should
+# use a very strong password otherwise it will be very easy to break.
+#
+# requirepass foobared
+
+# Command renaming.
+#
+# It is possible to change the name of dangerous commands in a shared
+# environment. For instance the CONFIG command may be renamed into something
+# hard to guess so that it will still be available for internal-use tools
+# but not available for general clients.
+#
+# Example:
+#
+# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
+#
+# It is also possible to completely kill a command by renaming it into
+# an empty string:
+#
+# rename-command CONFIG ""
+#
+# Please note that changing the name of commands that are logged into the
+# AOF file or transmitted to replicas may cause problems.
+
+################################### CLIENTS ####################################
+
+# Set the max number of connected clients at the same time. By default
+# this limit is set to 10000 clients, however if the Redis server is not
+# able to configure the process file limit to allow for the specified limit
+# the max number of allowed clients is set to the current file limit
+# minus 32 (as Redis reserves a few file descriptors for internal uses).
+#
+# Once the limit is reached Redis will close all the new connections sending
+# an error 'max number of clients reached'.
+#
+# maxclients 10000
+
+############################## MEMORY MANAGEMENT ################################
+
+# If Redis is to be used as an in-memory-only cache without any kind of
+# persistence, then the fork() mechanism used by the background AOF/RDB
+# persistence is unnecessary. As an optimization, all persistence can be
+# turned off in the Windows version of Redis. This will redirect heap
+# allocations to the system heap allocator, and disable commands that would
+# otherwise cause fork() operations: BGSAVE and BGREWRITEAOF.
+# This flag may not be combined with any of the other flags that configure
+# AOF and RDB operations.
+# persistence-available [(yes)|no]
+
+# Set a memory usage limit to the specified amount of bytes.
+# When the memory limit is reached Redis will try to remove keys
+# according to the eviction policy selected (see maxmemory-policy).
+#
+# If Redis can't remove keys according to the policy, or if the policy is
+# set to 'noeviction', Redis will start to reply with errors to commands
+# that would use more memory, like SET, LPUSH, and so on, and will continue
+# to reply to read-only commands like GET.
+#
+# This option is usually useful when using Redis as an LRU or LFU cache, or to
+# set a hard memory limit for an instance (using the 'noeviction' policy).
+#
+# WARNING: If you have replicas attached to an instance with maxmemory on,
+# the size of the output buffers needed to feed the replicas are subtracted
+# from the used memory count, so that network problems / resyncs will
+# not trigger a loop where keys are evicted, and in turn the output
+# buffer of replicas is full with DELs of keys evicted triggering the deletion
+# of more keys, and so forth until the database is completely emptied.
+#
+# In short... if you have replicas attached it is suggested that you set a lower
+# limit for maxmemory so that there is some free RAM on the system for replica
+# output buffers (but this is not needed if the policy is 'noeviction').
+#
+# WARNING: not setting maxmemory will cause Redis to terminate with an
+# out-of-memory exception if the heap limit is reached.
+#
+# NOTE: since Redis uses the system paging file to allocate the heap memory,
+# the Working Set memory usage showed by the Windows Task Manager or by other
+# tools such as ProcessExplorer will not always be accurate. For example, right
+# after a background save of the RDB or the AOF files, the working set value
+# may drop significantly. In order to check the correct amount of memory used
+# by the redis-server to store the data, use the INFO client command. The INFO
+# command shows only the memory used to store the redis data, not the extra
+# memory used by the Windows process for its own requirements. Th3 extra amount
+# of memory not reported by the INFO command can be calculated subtracting the
+# Peak Working Set reported by the Windows Task Manager and the used_memory_peak
+# reported by the INFO command.
+#
+# maxmemory <bytes>
+
+# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
+# is reached. You can select among five behaviors:
+#
+# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
+# allkeys-lru -> Evict any key using approximated LRU.
+# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
+# allkeys-lfu -> Evict any key using approximated LFU.
+# volatile-random -> Remove a random key among the ones with an expire set.
+# allkeys-random -> Remove a random key, any key.
+# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
+# noeviction -> Don't evict anything, just return an error on write operations.
+#
+# LRU means Least Recently Used
+# LFU means Least Frequently Used
+#
+# Both LRU, LFU and volatile-ttl are implemented using approximated
+# randomized algorithms.
+#
+# Note: with any of the above policies, Redis will return an error on write
+# operations, when there are no suitable keys for eviction.
+#
+# At the date of writing these commands are: set setnx setex append
+# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
+# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
+# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
+# getset mset msetnx exec sort
+#
+# The default is:
+#
+# maxmemory-policy noeviction
+
+# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
+# algorithms (in order to save memory), so you can tune it for speed or
+# accuracy. For default Redis will check five keys and pick the one that was
+# used less recently, you can change the sample size using the following
+# configuration directive.
+#
+# The default of 5 produces good enough results. 10 Approximates very closely
+# true LRU but costs more CPU. 3 is faster but not very accurate.
+#
+# maxmemory-samples 5
+
+# Starting from Redis 5, by default a replica will ignore its maxmemory setting
+# (unless it is promoted to master after a failover or manually). It means
+# that the eviction of keys will be just handled by the master, sending the
+# DEL commands to the replica as keys evict in the master side.
+#
+# This behavior ensures that masters and replicas stay consistent, and is usually
+# what you want, however if your replica is writable, or you want the replica to have
+# a different memory setting, and you are sure all the writes performed to the
+# replica are idempotent, then you may change this default (but be sure to understand
+# what you are doing).
+#
+# Note that since the replica by default does not evict, it may end using more
+# memory than the one set via maxmemory (there are certain buffers that may
+# be larger on the replica, or data structures may sometimes take more memory and so
+# forth). So make sure you monitor your replicas and make sure they have enough
+# memory to never hit a real out-of-memory condition before the master hits
+# the configured maxmemory setting.
+#
+# replica-ignore-maxmemory yes
+
+############################# LAZY FREEING ####################################
+
+# Redis has two primitives to delete keys. One is called DEL and is a blocking
+# deletion of the object. It means that the server stops processing new commands
+# in order to reclaim all the memory associated with an object in a synchronous
+# way. If the key deleted is associated with a small object, the time needed
+# in order to execute the DEL command is very small and comparable to most other
+# O(1) or O(log_N) commands in Redis. However if the key is associated with an
+# aggregated value containing millions of elements, the server can block for
+# a long time (even seconds) in order to complete the operation.
+#
+# For the above reasons Redis also offers non blocking deletion primitives
+# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and
+# FLUSHDB commands, in order to reclaim memory in background. Those commands
+# are executed in constant time. Another thread will incrementally free the
+# object in the background as fast as possible.
+#
+# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.
+# It's up to the design of the application to understand when it is a good
+# idea to use one or the other. However the Redis server sometimes has to
+# delete keys or flush the whole database as a side effect of other operations.
+# Specifically Redis deletes objects independently of a user call in the
+# following scenarios:
+#
+# 1) On eviction, because of the maxmemory and maxmemory policy configurations,
+# in order to make room for new data, without going over the specified
+# memory limit.
+# 2) Because of expire: when a key with an associated time to live (see the
+# EXPIRE command) must be deleted from memory.
+# 3) Because of a side effect of a command that stores data on a key that may
+# already exist. For example the RENAME command may delete the old key
+# content when it is replaced with another one. Similarly SUNIONSTORE
+# or SORT with STORE option may delete existing keys. The SET command
+# itself removes any old content of the specified key in order to replace
+# it with the specified string.
+# 4) During replication, when a replica performs a full resynchronization with
+# its master, the content of the whole database is removed in order to
+# load the RDB file just transferred.
+#
+# In all the above cases the default is to delete objects in a blocking way,
+# like if DEL was called. However you can configure each case specifically
+# in order to instead release memory in a non-blocking way like if UNLINK
+# was called, using the following configuration directives:
+
+lazyfree-lazy-eviction no
+lazyfree-lazy-expire no
+lazyfree-lazy-server-del no
+replica-lazy-flush no
+
+############################## APPEND ONLY MODE ###############################
+
+# By default Redis asynchronously dumps the dataset on disk. This mode is
+# good enough in many applications, but an issue with the Redis process or
+# a power outage may result into a few minutes of writes lost (depending on
+# the configured save points).
+#
+# The Append Only File is an alternative persistence mode that provides
+# much better durability. For instance using the default data fsync policy
+# (see later in the config file) Redis can lose just one second of writes in a
+# dramatic event like a server power outage, or a single write if something
+# wrong with the Redis process itself happens, but the operating system is
+# still running correctly.
+#
+# AOF and RDB persistence can be enabled at the same time without problems.
+# If the AOF is enabled on startup Redis will load the AOF, that is the file
+# with the better durability guarantees.
+#
+# Please check http://redis.io/topics/persistence for more information.
+
+appendonly no
+
+# The name of the append only file (default: "appendonly.aof")
+
+appendfilename "appendonly.aof"
+
+# The fsync() call tells the Operating System to actually write data on disk
+# instead of waiting for more data in the output buffer. Some OS will really flush
+# data on disk, some other OS will just try to do it ASAP.
+#
+# Redis supports three different modes:
+#
+# no: don't fsync, just let the OS flush the data when it wants. Faster.
+# always: fsync after every write to the append only log. Slow, Safest.
+# everysec: fsync only one time every second. Compromise.
+#
+# The default is "everysec", as that's usually the right compromise between
+# speed and data safety. It's up to you to understand if you can relax this to
+# "no" that will let the operating system flush the output buffer when
+# it wants, for better performances (but if you can live with the idea of
+# some data loss consider the default persistence mode that's snapshotting),
+# or on the contrary, use "always" that's very slow but a bit safer than
+# everysec.
+#
+# More details please check the following article:
+# http://antirez.com/post/redis-persistence-demystified.html
+#
+# If unsure, use "everysec".
+
+# appendfsync always
+appendfsync everysec
+# appendfsync no
+
+# When the AOF fsync policy is set to always or everysec, and a background
+# saving process (a background save or AOF log background rewriting) is
+# performing a lot of I/O against the disk, in some Linux configurations
+# Redis may block too long on the fsync() call. Note that there is no fix for
+# this currently, as even performing fsync in a different thread will block
+# our synchronous write(2) call.
+#
+# In order to mitigate this problem it's possible to use the following option
+# that will prevent fsync() from being called in the main process while a
+# BGSAVE or BGREWRITEAOF is in progress.
+#
+# This means that while another child is saving, the durability of Redis is
+# the same as "appendfsync none". In practical terms, this means that it is
+# possible to lose up to 30 seconds of log in the worst scenario (with the
+# default Linux settings).
+#
+# If you have latency problems turn this to "yes". Otherwise leave it as
+# "no" that is the safest pick from the point of view of durability.
+
+no-appendfsync-on-rewrite no
+
+# Automatic rewrite of the append only file.
+# Redis is able to automatically rewrite the log file implicitly calling
+# BGREWRITEAOF when the AOF log size grows by the specified percentage.
+#
+# This is how it works: Redis remembers the size of the AOF file after the
+# latest rewrite (if no rewrite has happened since the restart, the size of
+# the AOF at startup is used).
+#
+# This base size is compared to the current size. If the current size is
+# bigger than the specified percentage, the rewrite is triggered. Also
+# you need to specify a minimal size for the AOF file to be rewritten, this
+# is useful to avoid rewriting the AOF file even if the percentage increase
+# is reached but it is still pretty small.
+#
+# Specify a percentage of zero in order to disable the automatic AOF
+# rewrite feature.
+
+auto-aof-rewrite-percentage 100
+auto-aof-rewrite-min-size 64mb
+
+# An AOF file may be found to be truncated at the end during the Redis
+# startup process, when the AOF data gets loaded back into memory.
+# This may happen when the system where Redis is running
+# crashes, especially when an ext4 filesystem is mounted without the
+# data=ordered option (however this can't happen when Redis itself
+# crashes or aborts but the operating system still works correctly).
+#
+# Redis can either exit with an error when this happens, or load as much
+# data as possible (the default now) and start if the AOF file is found
+# to be truncated at the end. The following option controls this behavior.
+#
+# If aof-load-truncated is set to yes, a truncated AOF file is loaded and
+# the Redis server starts emitting a log to inform the user of the event.
+# Otherwise if the option is set to no, the server aborts with an error
+# and refuses to start. When the option is set to no, the user requires
+# to fix the AOF file using the "redis-check-aof" utility before to restart
+# the server.
+#
+# Note that if the AOF file will be found to be corrupted in the middle
+# the server will still exit with an error. This option only applies when
+# Redis will try to read more data from the AOF file but not enough bytes
+# will be found.
+aof-load-truncated yes
+
+# When rewriting the AOF file, Redis is able to use an RDB preamble in the
+# AOF file for faster rewrites and recoveries. When this option is turned
+# on the rewritten AOF file is composed of two different stanzas:
+#
+# [RDB file][AOF tail]
+#
+# When loading Redis recognizes that the AOF file starts with the "REDIS"
+# string and loads the prefixed RDB file, and continues loading the AOF
+# tail.
+aof-use-rdb-preamble yes
+
+################################ LUA SCRIPTING ###############################
+
+# Max execution time of a Lua script in milliseconds.
+#
+# If the maximum execution time is reached Redis will log that a script is
+# still in execution after the maximum allowed time and will start to
+# reply to queries with an error.
+#
+# When a long running script exceeds the maximum execution time only the
+# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
+# used to stop a script that did not yet called write commands. The second
+# is the only way to shut down the server in the case a write command was
+# already issued by the script but the user doesn't want to wait for the natural
+# termination of the script.
+#
+# Set it to 0 or a negative value for unlimited execution without warnings.
+lua-time-limit 5000
+
+################################ REDIS CLUSTER ###############################
+
+# Normal Redis instances can't be part of a Redis Cluster; only nodes that are
+# started as cluster nodes can. In order to start a Redis instance as a
+# cluster node enable the cluster support uncommenting the following:
+#
+# cluster-enabled yes
+
+# Every cluster node has a cluster configuration file. This file is not
+# intended to be edited by hand. It is created and updated by Redis nodes.
+# Every Redis Cluster node requires a different cluster configuration file.
+# Make sure that instances running in the same system do not have
+# overlapping cluster configuration file names.
+#
+# cluster-config-file nodes-6379.conf
+
+# Cluster node timeout is the amount of milliseconds a node must be unreachable
+# for it to be considered in failure state.
+# Most other internal time limits are multiple of the node timeout.
+#
+# cluster-node-timeout 15000
+
+# A replica of a failing master will avoid to start a failover if its data
+# looks too old.
+#
+# There is no simple way for a replica to actually have an exact measure of
+# its "data age", so the following two checks are performed:
+#
+# 1) If there are multiple replicas able to failover, they exchange messages
+# in order to try to give an advantage to the replica with the best
+# replication offset (more data from the master processed).
+# Replicas will try to get their rank by offset, and apply to the start
+# of the failover a delay proportional to their rank.
+#
+# 2) Every single replica computes the time of the last interaction with
+# its master. This can be the last ping or command received (if the master
+# is still in the "connected" state), or the time that elapsed since the
+# disconnection with the master (if the replication link is currently down).
+# If the last interaction is too old, the replica will not try to failover
+# at all.
+#
+# The point "2" can be tuned by user. Specifically a replica will not perform
+# the failover if, since the last interaction with the master, the time
+# elapsed is greater than:
+#
+# (node-timeout * replica-validity-factor) + repl-ping-replica-period
+#
+# So for example if node-timeout is 30 seconds, and the replica-validity-factor
+# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the
+# replica will not try to failover if it was not able to talk with the master
+# for longer than 310 seconds.
+#
+# A large replica-validity-factor may allow replicas with too old data to failover
+# a master, while a too small value may prevent the cluster from being able to
+# elect a replica at all.
+#
+# For maximum availability, it is possible to set the replica-validity-factor
+# to a value of 0, which means, that replicas will always try to failover the
+# master regardless of the last time they interacted with the master.
+# (However they'll always try to apply a delay proportional to their
+# offset rank).
+#
+# Zero is the only value able to guarantee that when all the partitions heal
+# the cluster will always be able to continue.
+#
+# cluster-replica-validity-factor 10
+
+# Cluster replicas are able to migrate to orphaned masters, that are masters
+# that are left without working replicas. This improves the cluster ability
+# to resist to failures as otherwise an orphaned master can't be failed over
+# in case of failure if it has no working replicas.
+#
+# Replicas migrate to orphaned masters only if there are still at least a
+# given number of other working replicas for their old master. This number
+# is the "migration barrier". A migration barrier of 1 means that a replica
+# will migrate only if there is at least 1 other working replica for its master
+# and so forth. It usually reflects the number of replicas you want for every
+# master in your cluster.
+#
+# Default is 1 (replicas migrate only if their masters remain with at least
+# one replica). To disable migration just set it to a very large value.
+# A value of 0 can be set but is useful only for debugging and dangerous
+# in production.
+#
+# cluster-migration-barrier 1
+
+# By default Redis Cluster nodes stop accepting queries if they detect there
+# is at least an hash slot uncovered (no available node is serving it).
+# This way if the cluster is partially down (for example a range of hash slots
+# are no longer covered) all the cluster becomes, eventually, unavailable.
+# It automatically returns available as soon as all the slots are covered again.
+#
+# However sometimes you want the subset of the cluster which is working,
+# to continue to accept queries for the part of the key space that is still
+# covered. In order to do so, just set the cluster-require-full-coverage
+# option to no.
+#
+# cluster-require-full-coverage yes
+
+# This option, when set to yes, prevents replicas from trying to failover its
+# master during master failures. However the master can still perform a
+# manual failover, if forced to do so.
+#
+# This is useful in different scenarios, especially in the case of multiple
+# data center operations, where we want one side to never be promoted if not
+# in the case of a total DC failure.
+#
+# cluster-replica-no-failover no
+
+# In order to setup your cluster make sure to read the documentation
+# available at http://redis.io web site.
+
+########################## CLUSTER DOCKER/NAT support ########################
+
+# In certain deployments, Redis Cluster nodes address discovery fails, because
+# addresses are NAT-ted or because ports are forwarded (the typical case is
+# Docker and other containers).
+#
+# In order to make Redis Cluster working in such environments, a static
+# configuration where each node knows its public address is needed. The
+# following two options are used for this scope, and are:
+#
+# * cluster-announce-ip
+# * cluster-announce-port
+# * cluster-announce-bus-port
+#
+# Each instruct the node about its address, client port, and cluster message
+# bus port. The information is then published in the header of the bus packets
+# so that other nodes will be able to correctly map the address of the node
+# publishing the information.
+#
+# If the above options are not used, the normal Redis Cluster auto-detection
+# will be used instead.
+#
+# Note that when remapped, the bus port may not be at the fixed offset of
+# clients port + 10000, so you can specify any port and bus-port depending
+# on how they get remapped. If the bus-port is not set, a fixed offset of
+# 10000 will be used as usually.
+#
+# Example:
+#
+# cluster-announce-ip 10.1.1.5
+# cluster-announce-port 6379
+# cluster-announce-bus-port 6380
+
+################################## SLOW LOG ###################################
+
+# The Redis Slow Log is a system to log queries that exceeded a specified
+# execution time. The execution time does not include the I/O operations
+# like talking with the client, sending the reply and so forth,
+# but just the time needed to actually execute the command (this is the only
+# stage of command execution where the thread is blocked and can not serve
+# other requests in the meantime).
+#
+# You can configure the slow log with two parameters: one tells Redis
+# what is the execution time, in microseconds, to exceed in order for the
+# command to get logged, and the other parameter is the length of the
+# slow log. When a new command is logged the oldest one is removed from the
+# queue of logged commands.
+
+# The following time is expressed in microseconds, so 1000000 is equivalent
+# to one second. Note that a negative number disables the slow log, while
+# a value of zero forces the logging of every command.
+slowlog-log-slower-than 10000
+
+# There is no limit to this length. Just be aware that it will consume memory.
+# You can reclaim memory used by the slow log with SLOWLOG RESET.
+slowlog-max-len 128
+
+################################ LATENCY MONITOR ##############################
+
+# The Redis latency monitoring subsystem samples different operations
+# at runtime in order to collect data related to possible sources of
+# latency of a Redis instance.
+#
+# Via the LATENCY command this information is available to the user that can
+# print graphs and obtain reports.
+#
+# The system only logs operations that were performed in a time equal or
+# greater than the amount of milliseconds specified via the
+# latency-monitor-threshold configuration directive. When its value is set
+# to zero, the latency monitor is turned off.
+#
+# By default latency monitoring is disabled since it is mostly not needed
+# if you don't have latency issues, and collecting data has a performance
+# impact, that while very small, can be measured under big load. Latency
+# monitoring can easily be enabled at runtime using the command
+# "CONFIG SET latency-monitor-threshold <milliseconds>" if needed.
+latency-monitor-threshold 0
+
+############################# EVENT NOTIFICATION ##############################
+
+# Redis can notify Pub/Sub clients about events happening in the key space.
+# This feature is documented at http://redis.io/topics/notifications
+#
+# For instance if keyspace events notification is enabled, and a client
+# performs a DEL operation on key "foo" stored in the Database 0, two
+# messages will be published via Pub/Sub:
+#
+# PUBLISH __keyspace@0__:foo del
+# PUBLISH __keyevent@0__:del foo
+#
+# It is possible to select the events that Redis will notify among a set
+# of classes. Every class is identified by a single character:
+#
+# K Keyspace events, published with __keyspace@<db>__ prefix.
+# E Keyevent events, published with __keyevent@<db>__ prefix.
+# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
+# $ String commands
+# l List commands
+# s Set commands
+# h Hash commands
+# z Sorted set commands
+# x Expired events (events generated every time a key expires)
+# e Evicted events (events generated when a key is evicted for maxmemory)
+# A Alias for g$lshzxe, so that the "AKE" string means all the events.
+#
+# The "notify-keyspace-events" takes as argument a string that is composed
+# of zero or multiple characters. The empty string means that notifications
+# are disabled.
+#
+# Example: to enable list and generic events, from the point of view of the
+# event name, use:
+#
+# notify-keyspace-events Elg
+#
+# Example 2: to get the stream of the expired keys subscribing to channel
+# name __keyevent@0__:expired use:
+#
+# notify-keyspace-events Ex
+#
+# By default all notifications are disabled because most users don't need
+# this feature and the feature has some overhead. Note that if you don't
+# specify at least one of K or E, no events will be delivered.
+notify-keyspace-events ""
+
+############################### ADVANCED CONFIG ###############################
+
+# Hashes are encoded using a memory efficient data structure when they have a
+# small number of entries, and the biggest entry does not exceed a given
+# threshold. These thresholds can be configured using the following directives.
+hash-max-ziplist-entries 512
+hash-max-ziplist-value 64
+
+# Lists are also encoded in a special way to save a lot of space.
+# The number of entries allowed per internal list node can be specified
+# as a fixed maximum size or a maximum number of elements.
+# For a fixed maximum size, use -5 through -1, meaning:
+# -5: max size: 64 Kb <-- not recommended for normal workloads
+# -4: max size: 32 Kb <-- not recommended
+# -3: max size: 16 Kb <-- probably not recommended
+# -2: max size: 8 Kb <-- good
+# -1: max size: 4 Kb <-- good
+# Positive numbers mean store up to _exactly_ that number of elements
+# per list node.
+# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
+# but if your use case is unique, adjust the settings as necessary.
+list-max-ziplist-size -2
+
+# Lists may also be compressed.
+# Compress depth is the number of quicklist ziplist nodes from *each* side of
+# the list to *exclude* from compression. The head and tail of the list
+# are always uncompressed for fast push/pop operations. Settings are:
+# 0: disable all list compression
+# 1: depth 1 means "don't start compressing until after 1 node into the list,
+# going from either the head or tail"
+# So: [head]->node->node->...->node->[tail]
+# [head], [tail] will always be uncompressed; inner nodes will compress.
+# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
+# 2 here means: don't compress head or head->next or tail->prev or tail,
+# but compress all nodes between them.
+# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
+# etc.
+list-compress-depth 0
+
+# Sets have a special encoding in just one case: when a set is composed
+# of just strings that happen to be integers in radix 10 in the range
+# of 64 bit signed integers.
+# The following configuration setting sets the limit in the size of the
+# set in order to use this special memory saving encoding.
+set-max-intset-entries 512
+
+# Similarly to hashes and lists, sorted sets are also specially encoded in
+# order to save a lot of space. This encoding is only used when the length and
+# elements of a sorted set are below the following limits:
+zset-max-ziplist-entries 128
+zset-max-ziplist-value 64
+
+# HyperLogLog sparse representation bytes limit. The limit includes the
+# 16 bytes header. When an HyperLogLog using the sparse representation crosses
+# this limit, it is converted into the dense representation.
+#
+# A value greater than 16000 is totally useless, since at that point the
+# dense representation is more memory efficient.
+#
+# The suggested value is ~ 3000 in order to have the benefits of
+# the space efficient encoding without slowing down too much PFADD,
+# which is O(N) with the sparse encoding. The value can be raised to
+# ~ 10000 when CPU is not a concern, but space is, and the data set is
+# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.
+hll-sparse-max-bytes 3000
+
+# Streams macro node max size / items. The stream data structure is a radix
+# tree of big nodes that encode multiple items inside. Using this configuration
+# it is possible to configure how big a single node can be in bytes, and the
+# maximum number of items it may contain before switching to a new node when
+# appending new stream entries. If any of the following settings are set to
+# zero, the limit is ignored, so for instance it is possible to set just a
+# max entires limit by setting max-bytes to 0 and max-entries to the desired
+# value.
+stream-node-max-bytes 4096
+stream-node-max-entries 100
+
+# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in
+# order to help rehashing the main Redis hash table (the one mapping top-level
+# keys to values). The hash table implementation Redis uses (see dict.c)
+# performs a lazy rehashing: the more operation you run into a hash table
+# that is rehashing, the more rehashing "steps" are performed, so if the
+# server is idle the rehashing is never complete and some more memory is used
+# by the hash table.
+#
+# The default is to use this millisecond 10 times every second in order to
+# actively rehash the main dictionaries, freeing memory when possible.
+#
+# If unsure:
+# use "activerehashing no" if you have hard latency requirements and it is
+# not a good thing in your environment that Redis can reply from time to time
+# to queries with 2 milliseconds delay.
+#
+# use "activerehashing yes" if you don't have such hard requirements but
+# want to free memory asap when possible.
+activerehashing yes
+
+# The client output buffer limits can be used to force disconnection of clients
+# that are not reading data from the server fast enough for some reason (a
+# common reason is that a Pub/Sub client can't consume messages as fast as the
+# publisher can produce them).
+#
+# The limit can be set differently for the three different classes of clients:
+#
+# normal -> normal clients including MONITOR clients
+# replica -> replica clients
+# pubsub -> clients subscribed to at least one pubsub channel or pattern
+#
+# The syntax of every client-output-buffer-limit directive is the following:
+#
+# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
+#
+# A client is immediately disconnected once the hard limit is reached, or if
+# the soft limit is reached and remains reached for the specified number of
+# seconds (continuously).
+# So for instance if the hard limit is 32 megabytes and the soft limit is
+# 16 megabytes / 10 seconds, the client will get disconnected immediately
+# if the size of the output buffers reach 32 megabytes, but will also get
+# disconnected if the client reaches 16 megabytes and continuously overcomes
+# the limit for 10 seconds.
+#
+# By default normal clients are not limited because they don't receive data
+# without asking (in a push way), but just after a request, so only
+# asynchronous clients may create a scenario where data is requested faster
+# than it can read.
+#
+# Instead there is a default limit for pubsub and replica clients, since
+# subscribers and replicas receive data in a push fashion.
+#
+# Both the hard or the soft limit can be disabled by setting them to zero.
+client-output-buffer-limit normal 0 0 0
+client-output-buffer-limit replica 256mb 64mb 60
+client-output-buffer-limit pubsub 32mb 8mb 60
+
+# Client query buffers accumulate new commands. They are limited to a fixed
+# amount by default in order to avoid that a protocol desynchronization (for
+# instance due to a bug in the client) will lead to unbound memory usage in
+# the query buffer. However you can configure it here if you have very special
+# needs, such us huge multi/exec requests or alike.
+#
+# client-query-buffer-limit 1gb
+
+# In the Redis protocol, bulk requests, that are, elements representing single
+# strings, are normally limited ot 512 mb. However you can change this limit
+# here.
+#
+# proto-max-bulk-len 512mb
+
+# Redis calls an internal function to perform many background tasks, like
+# closing connections of clients in timeout, purging expired keys that are
+# never requested, and so forth.
+#
+# Not all tasks are performed with the same frequency, but Redis checks for
+# tasks to perform according to the specified "hz" value.
+#
+# By default "hz" is set to 10. Raising the value will use more CPU when
+# Redis is idle, but at the same time will make Redis more responsive when
+# there are many keys expiring at the same time, and timeouts may be
+# handled with more precision.
+#
+# The range is between 1 and 500, however a value over 100 is usually not
+# a good idea. Most users should use the default of 10 and raise this up to
+# 100 only in environments where very low latency is required.
+hz 10
+
+# Normally it is useful to have an HZ value which is proportional to the
+# number of clients connected. This is useful in order, for instance, to
+# avoid too many clients are processed for each background task invocation
+# in order to avoid latency spikes.
+#
+# Since the default HZ value by default is conservatively set to 10, Redis
+# offers, and enables by default, the ability to use an adaptive HZ value
+# which will temporary raise when there are many connected clients.
+#
+# When dynamic HZ is enabled, the actual configured HZ will be used as
+# as a baseline, but multiples of the configured HZ value will be actually
+# used as needed once more clients are connected. In this way an idle
+# instance will use very little CPU time while a busy instance will be
+# more responsive.
+dynamic-hz yes
+
+# When a child rewrites the AOF file, if the following option is enabled
+# the file will be fsync-ed every 32 MB of data generated. This is useful
+# in order to commit the file to the disk more incrementally and avoid
+# big latency spikes.
+aof-rewrite-incremental-fsync yes
+
+# When redis saves RDB file, if the following option is enabled
+# the file will be fsync-ed every 32 MB of data generated. This is useful
+# in order to commit the file to the disk more incrementally and avoid
+# big latency spikes.
+rdb-save-incremental-fsync yes
+
+# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good
+# idea to start with the default settings and only change them after investigating
+# how to improve the performances and how the keys LFU change over time, which
+# is possible to inspect via the OBJECT FREQ command.
+#
+# There are two tunable parameters in the Redis LFU implementation: the
+# counter logarithm factor and the counter decay time. It is important to
+# understand what the two parameters mean before changing them.
+#
+# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis
+# uses a probabilistic increment with logarithmic behavior. Given the value
+# of the old counter, when a key is accessed, the counter is incremented in
+# this way:
+#
+# 1. A random number R between 0 and 1 is extracted.
+# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).
+# 3. The counter is incremented only if R < P.
+#
+# The default lfu-log-factor is 10. This is a table of how the frequency
+# counter changes with a different number of accesses with different
+# logarithmic factors:
+#
+# +--------+------------+------------+------------+------------+------------+
+# | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
+# +--------+------------+------------+------------+------------+------------+
+# | 0 | 104 | 255 | 255 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 1 | 18 | 49 | 255 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 10 | 10 | 18 | 142 | 255 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+# | 100 | 8 | 11 | 49 | 143 | 255 |
+# +--------+------------+------------+------------+------------+------------+
+#
+# NOTE: The above table was obtained by running the following commands:
+#
+# redis-benchmark -n 1000000 incr foo
+# redis-cli object freq foo
+#
+# NOTE 2: The counter initial value is 5 in order to give new objects a chance
+# to accumulate hits.
+#
+# The counter decay time is the time, in minutes, that must elapse in order
+# for the key counter to be divided by two (or decremented if it has a value
+# less <= 10).
+#
+# The default value for the lfu-decay-time is 1. A Special value of 0 means to
+# decay the counter every time it happens to be scanned.
+#
+# lfu-log-factor 10
+# lfu-decay-time 1
+
+################################## INCLUDES ###################################
+
+# Include one or more other config files here. This is useful if you
+# have a standard template that goes to all Redis server but also need
+# to customize a few per-server settings. Include files can include
+# other files, so use this wisely.
+#
+# include /path/to/local.conf
+# include /path/to/other.conf
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..61d8a71
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ThunderHubServer</artifactId>
+ <version>3.8.8</version>
+
+ <name>ruoyi</name>
+ <url>http://www.ruoyi.vip</url>
+ <description>若依管理系统</description>
+
+ <properties>
+ <ruoyi.version>3.8.8</ruoyi.version>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <java.version>17</java.version>
+ <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
+ <mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
+ <druid.version>1.2.23</druid.version>
+ <bitwalker.version>1.21</bitwalker.version>
+ <swagger.version>3.0.0</swagger.version>
+ <kaptcha.version>2.3.3</kaptcha.version>
+ <pagehelper.boot.version>2.1.0</pagehelper.boot.version>
+ <fastjson.version>2.0.43</fastjson.version>
+ <oshi.version>6.6.3</oshi.version>
+ <commons.io.version>2.13.0</commons.io.version>
+ <poi.version>4.1.2</poi.version>
+ <velocity.version>2.3</velocity.version>
+ <jwt.version>0.9.1</jwt.version>
+ <mysql.version>8.2.0</mysql.version>
+ <jaxb-api.version>2.3.1</jaxb-api.version>
+ <jakarta.version>6.0.0</jakarta.version>
+ <springdoc.version>2.5.0</springdoc.version>
+ </properties>
+
+ <!-- 依赖声明 -->
+ <dependencyManagement>
+ <dependencies>
+
+ <!-- SpringBoot的依赖配置-->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-dependencies</artifactId>
+ <version>3.3.0</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+
+ <!-- 阿里数据库连接池 -->
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>druid-spring-boot-3-starter</artifactId>
+ <version>${druid.version}</version>
+ </dependency>
+
+ <!-- 解析客户端操作系统、浏览器等 -->
+ <dependency>
+ <groupId>eu.bitwalker</groupId>
+ <artifactId>UserAgentUtils</artifactId>
+ <version>${bitwalker.version}</version>
+ </dependency>
+
+ <!-- pagehelper 分页插件 -->
+ <dependency>
+ <groupId>com.github.pagehelper</groupId>
+ <artifactId>pagehelper-spring-boot-starter</artifactId>
+ <version>${pagehelper.boot.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mybatis.spring.boot</groupId>
+ <artifactId>mybatis-spring-boot-starter</artifactId>
+ <version>${mybatis-spring-boot.version}</version>
+ </dependency>
+
+
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ <version>${mysql.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.xml.bind</groupId>
+ <artifactId>jaxb-api</artifactId>
+ <version>${jaxb-api.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>${jakarta.version}</version>
+ </dependency>
+
+ <!-- 获取系统信息 -->
+ <dependency>
+ <groupId>com.github.oshi</groupId>
+ <artifactId>oshi-core</artifactId>
+ <version>${oshi.version}</version>
+ </dependency>
+
+ <!-- spring-doc -->
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+ <version>${springdoc.version}</version>
+ </dependency>
+
+ <!-- io常用工具类 -->
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>${commons.io.version}</version>
+ </dependency>
+
+ <!-- excel工具 -->
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml</artifactId>
+ <version>${poi.version}</version>
+ </dependency>
+
+ <!-- velocity代码生成使用模板 -->
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity-engine-core</artifactId>
+ <version>${velocity.version}</version>
+ </dependency>
+
+ <!-- 阿里JSON解析器 -->
+ <dependency>
+ <groupId>com.alibaba.fastjson2</groupId>
+ <artifactId>fastjson2</artifactId>
+ <version>${fastjson.version}</version>
+ </dependency>
+
+ <!-- Token生成与解析-->
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt</artifactId>
+ <version>${jwt.version}</version>
+ </dependency>
+
+ <!-- 验证码 -->
+ <dependency>
+ <groupId>pro.fessional</groupId>
+ <artifactId>kaptcha</artifactId>
+ <version>${kaptcha.version}</version>
+ </dependency>
+
+ <!-- 定时任务-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-quartz</artifactId>
+ <version>${ruoyi.version}</version>
+ </dependency>
+
+ <!-- 代码生成-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-generator</artifactId>
+ <version>${ruoyi.version}</version>
+ </dependency>
+
+ <!-- 核心模块-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-framework</artifactId>
+ <version>${ruoyi.version}</version>
+ </dependency>
+
+ <!-- 系统模块-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-system</artifactId>
+ <version>${ruoyi.version}</version>
+ </dependency>
+
+ <!-- 通用工具-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ <version>${ruoyi.version}</version>
+ </dependency>
+
+<!-- junit5 配置-->
+
+ <dependency>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <version>1.0.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.0.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.vintage</groupId>
+ <artifactId>junit-vintage-engine</artifactId>
+ <version>4.12.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+ </dependencyManagement>
+
+ <modules>
+ <module>ruoyi-admin</module>
+ <module>ruoyi-framework</module>
+ <module>ruoyi-system</module>
+ <module>ruoyi-quartz</module>
+ <module>ruoyi-generator</module>
+ <module>ruoyi-common</module>
+ </modules>
+ <packaging>pom</packaging>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.13.0</version>
+ <configuration>
+ <parameters>true</parameters>
+ <source>${java.version}</source>
+ <target>${java.version}</target>
+ <encoding>${project.build.sourceEncoding}</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>3.3.0</version>
+ </plugin>
+ </plugins>
+ </build>
+
+ <repositories>
+ <repository>
+ <id>public</id>
+ <name>aliyun nexus</name>
+ <url>https://maven.aliyun.com/repository/public</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ </repository>
+ </repositories>
+
+ <pluginRepositories>
+ <pluginRepository>
+ <id>public</id>
+ <name>aliyun nexus</name>
+ <url>https://maven.aliyun.com/repository/public</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </pluginRepository>
+ </pluginRepositories>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-admin/download/test.torrent b/ruoyi-admin/download/test.torrent
new file mode 100644
index 0000000..e4503c3
--- /dev/null
+++ b/ruoyi-admin/download/test.torrent
@@ -0,0 +1 @@
+I noly want to test it
\ No newline at end of file
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
new file mode 100644
index 0000000..8053e2b
--- /dev/null
+++ b/ruoyi-admin/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <packaging>jar</packaging>
+ <artifactId>ruoyi-admin</artifactId>
+
+ <description>
+ web服务入口
+ </description>
+
+ <dependencies>
+
+ <!-- spring-boot-devtools -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-devtools</artifactId>
+ <optional>true</optional> <!-- 表示依赖不会传递 -->
+ </dependency>
+
+ <!-- spring-doc -->
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+ </dependency>
+
+ <!-- Mysql驱动包 -->
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ </dependency>
+
+ <!--mybatis-plus的springboot支持-->
+<!-- <dependency>-->
+<!-- <groupId> 导入冲突 com.baomidou</groupId>-->
+<!-- <artifactId>mybatis-plus-boot-starter</artifactId>-->
+<!-- <version>3.5.10.1</version>-->
+<!-- </dependency>-->
+
+ <dependency>
+ <groupId>com.turn.ttorrent</groupId>
+ <artifactId>ttorrent-cli-shaded</artifactId>
+ <version>1.3</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/torrent/ttorrent-tracker.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.turn.ttorrent</groupId>
+ <artifactId>ttorrent-client</artifactId>
+ <version>1.3</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/torrent/ttorrent-client.jar</systemPath>
+ </dependency>
+
+ <!-- 核心模块-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-framework</artifactId>
+ </dependency>
+
+ <!-- 定时任务-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-quartz</artifactId>
+ </dependency>
+
+ <!-- 代码生成-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-generator</artifactId>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>2.5.15</version>
+ <configuration>
+ <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>repackage</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ <version>3.1.0</version>
+ <configuration>
+ <failOnMissingWebXml>false</failOnMissingWebXml>
+ <warName>${project.artifactId}</warName>
+ </configuration>
+ </plugin>
+ </plugins>
+ <finalName>${project.artifactId}</finalName>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
new file mode 100644
index 0000000..a102b45
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
@@ -0,0 +1,26 @@
+package com.ruoyi;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+
+/**
+ * 启动程序
+ *
+ * @author ruoyi
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+public class RuoYiApplication
+{
+ public static void main(String[] args)
+ {
+ // System.setProperty("spring.devtools.restart.enabled", "false");
+ SpringApplication.run(RuoYiApplication.class, args);
+ // ANSI escape code for blue text
+ String blue = "\u001B[34m";
+ // ANSI escape code to reset text color
+ String reset = "\u001B[0m";
+
+ System.out.println(blue + " ----赵雨滔香香香启动成功 喵喵喵" + reset);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java
new file mode 100644
index 0000000..6de67dc
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java
@@ -0,0 +1,18 @@
+package com.ruoyi;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * web容器中进行部署
+ *
+ * @author ruoyi
+ */
+public class RuoYiServletInitializer extends SpringBootServletInitializer
+{
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+ {
+ return application.sources(RuoYiApplication.class);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
new file mode 100644
index 0000000..d6dcf06
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
@@ -0,0 +1,94 @@
+package com.ruoyi.web.controller.common;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import jakarta.annotation.Resource;
+import javax.imageio.ImageIO;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.google.code.kaptcha.Producer;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.sign.Base64;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 验证码操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+public class CaptchaController
+{
+ @Resource(name = "captchaProducer")
+ private Producer captchaProducer;
+
+ @Resource(name = "captchaProducerMath")
+ private Producer captchaProducerMath;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @Autowired
+ private ISysConfigService configService;
+ /**
+ * 生成验证码
+ */
+ @GetMapping("/captchaImage")
+ public AjaxResult getCode(HttpServletResponse response) throws IOException
+ {
+ AjaxResult ajax = AjaxResult.success();
+ boolean captchaEnabled = configService.selectCaptchaEnabled();
+ ajax.put("captchaEnabled", captchaEnabled);
+ if (!captchaEnabled)
+ {
+ return ajax;
+ }
+
+ // 保存验证码信息
+ String uuid = IdUtils.simpleUUID();
+ String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
+
+ String capStr = null, code = null;
+ BufferedImage image = null;
+
+ // 生成验证码
+ String captchaType = RuoYiConfig.getCaptchaType();
+ if ("math".equals(captchaType))
+ {
+ String capText = captchaProducerMath.createText();
+ capStr = capText.substring(0, capText.lastIndexOf("@"));
+ code = capText.substring(capText.lastIndexOf("@") + 1);
+ image = captchaProducerMath.createImage(capStr);
+ }
+ else if ("char".equals(captchaType))
+ {
+ capStr = code = captchaProducer.createText();
+ image = captchaProducer.createImage(capStr);
+ }
+
+ redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+ // 转换流信息写出
+ FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+ try
+ {
+ ImageIO.write(image, "jpg", os);
+ }
+ catch (IOException e)
+ {
+ return AjaxResult.error(e.getMessage());
+ }
+
+ ajax.put("uuid", uuid);
+ ajax.put("img", Base64.encode(os.toByteArray()));
+ return ajax;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
new file mode 100644
index 0000000..b7fad0c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
@@ -0,0 +1,163 @@
+package com.ruoyi.web.controller.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.framework.config.ServerConfig;
+
+/**
+ * 通用请求处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/common")
+public class CommonController
+{
+ private static final Logger log = LoggerFactory.getLogger(CommonController.class);
+
+ @Autowired
+ private ServerConfig serverConfig;
+
+ private static final String FILE_DELIMETER = ",";
+
+ /**
+ * 通用下载请求
+ *
+ * @param fileName 文件名称
+ * @param delete 是否删除
+ */
+ @GetMapping("/download")
+ public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
+ {
+ try
+ {
+ if (!FileUtils.checkAllowDownload(fileName))
+ {
+ throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+ }
+ String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
+ String filePath = RuoYiConfig.getDownloadPath() + fileName;
+
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ FileUtils.setAttachmentResponseHeader(response, realFileName);
+ FileUtils.writeBytes(filePath, response.getOutputStream());
+ if (delete)
+ {
+ FileUtils.deleteFile(filePath);
+ }
+ }
+ catch (Exception e)
+ {
+ log.error("下载文件失败", e);
+ }
+ }
+
+ /**
+ * 通用上传请求(单个)
+ */
+ @PostMapping("/upload")
+ public AjaxResult uploadFile(MultipartFile file) throws Exception
+ {
+ try
+ {
+ // 上传文件路径
+ String filePath = RuoYiConfig.getUploadPath();
+ // 上传并返回新文件名称
+ String fileName = FileUploadUtils.upload(filePath, file);
+ String url = serverConfig.getUrl() + fileName;
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("url", url);
+ ajax.put("fileName", fileName);
+ ajax.put("newFileName", FileUtils.getName(fileName));
+ ajax.put("originalFilename", file.getOriginalFilename());
+ return ajax;
+ }
+ catch (Exception e)
+ {
+ return AjaxResult.error(e.getMessage());
+ }
+ }
+
+ /**
+ * 通用上传请求(多个)
+ */
+ @PostMapping("/uploads")
+ public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
+ {
+ try
+ {
+ // 上传文件路径
+ String filePath = RuoYiConfig.getUploadPath();
+ List<String> urls = new ArrayList<String>();
+ List<String> fileNames = new ArrayList<String>();
+ List<String> newFileNames = new ArrayList<String>();
+ List<String> originalFilenames = new ArrayList<String>();
+ for (MultipartFile file : files)
+ {
+ // 上传并返回新文件名称
+ String fileName = FileUploadUtils.upload(filePath, file);
+ String url = serverConfig.getUrl() + fileName;
+ urls.add(url);
+ fileNames.add(fileName);
+ newFileNames.add(FileUtils.getName(fileName));
+ originalFilenames.add(file.getOriginalFilename());
+ }
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
+ ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
+ ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
+ ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
+ return ajax;
+ }
+ catch (Exception e)
+ {
+ return AjaxResult.error(e.getMessage());
+ }
+ }
+
+ /**
+ * 本地资源通用下载
+ */
+ @GetMapping("/download/resource")
+ public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
+ throws Exception
+ {
+ try
+ {
+ if (!FileUtils.checkAllowDownload(resource))
+ {
+ throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+ }
+ // 本地资源路径
+ String localPath = RuoYiConfig.getProfile();
+ // 数据库资源地址
+ String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
+ // 下载名称
+ String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ FileUtils.setAttachmentResponseHeader(response, downloadName);
+ FileUtils.writeBytes(downloadPath, response.getOutputStream());
+ }
+ catch (Exception e)
+ {
+ log.error("下载文件失败", e);
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
new file mode 100644
index 0000000..d17bd66
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
@@ -0,0 +1,122 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysCache;
+
+/**
+ * 缓存监控
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/cache")
+public class CacheController
+{
+ @Autowired
+ private RedisTemplate<String, String> redisTemplate;
+
+ private final static List<SysCache> caches = new ArrayList<SysCache>();
+ {
+ caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
+ caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
+ caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
+ caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
+ caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
+ caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
+ caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
+ }
+
+ @SuppressWarnings("deprecation")
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @GetMapping()
+ public AjaxResult getInfo() throws Exception
+ {
+ Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
+ Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
+ Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
+
+ Map<String, Object> result = new HashMap<>(3);
+ result.put("info", info);
+ result.put("dbSize", dbSize);
+
+ List<Map<String, String>> pieList = new ArrayList<>();
+ commandStats.stringPropertyNames().forEach(key -> {
+ Map<String, String> data = new HashMap<>(2);
+ String property = commandStats.getProperty(key);
+ data.put("name", StringUtils.removeStart(key, "cmdstat_"));
+ data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
+ pieList.add(data);
+ });
+ result.put("commandStats", pieList);
+ return AjaxResult.success(result);
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @GetMapping("/getNames")
+ public AjaxResult cache()
+ {
+ return AjaxResult.success(caches);
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @GetMapping("/getKeys/{cacheName}")
+ public AjaxResult getCacheKeys(@PathVariable String cacheName)
+ {
+ Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+ return AjaxResult.success(new TreeSet<>(cacheKeys));
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @GetMapping("/getValue/{cacheName}/{cacheKey}")
+ public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
+ {
+ String cacheValue = redisTemplate.opsForValue().get(cacheKey);
+ SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
+ return AjaxResult.success(sysCache);
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @DeleteMapping("/clearCacheName/{cacheName}")
+ public AjaxResult clearCacheName(@PathVariable String cacheName)
+ {
+ Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+ redisTemplate.delete(cacheKeys);
+ return AjaxResult.success();
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @DeleteMapping("/clearCacheKey/{cacheKey}")
+ public AjaxResult clearCacheKey(@PathVariable String cacheKey)
+ {
+ redisTemplate.delete(cacheKey);
+ return AjaxResult.success();
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+ @DeleteMapping("/clearCacheAll")
+ public AjaxResult clearCacheAll()
+ {
+ Collection<String> cacheKeys = redisTemplate.keys("*");
+ redisTemplate.delete(cacheKeys);
+ return AjaxResult.success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
new file mode 100644
index 0000000..cc805ad
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
@@ -0,0 +1,27 @@
+package com.ruoyi.web.controller.monitor;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.framework.web.domain.Server;
+
+/**
+ * 服务器监控
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/server")
+public class ServerController
+{
+ @PreAuthorize("@ss.hasPermi('monitor:server:list')")
+ @GetMapping()
+ public AjaxResult getInfo() throws Exception
+ {
+ Server server = new Server();
+ server.copyTo();
+ return AjaxResult.success(server);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java
new file mode 100644
index 0000000..f6fac71
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java
@@ -0,0 +1,82 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.framework.web.service.SysPasswordService;
+import com.ruoyi.system.domain.SysLogininfor;
+import com.ruoyi.system.service.ISysLogininforService;
+
+/**
+ * 系统访问记录
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/logininfor")
+public class SysLogininforController extends BaseController
+{
+ @Autowired
+ private ISysLogininforService logininforService;
+
+ @Autowired
+ private SysPasswordService passwordService;
+
+ @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysLogininfor logininfor)
+ {
+ startPage();
+ List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+ return getDataTable(list);
+ }
+
+ @Log(title = "登录日志", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysLogininfor logininfor)
+ {
+ List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+ ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
+ util.exportExcel(response, list, "登录日志");
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+ @Log(title = "登录日志", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{infoIds}")
+ public AjaxResult remove(@PathVariable Long[] infoIds)
+ {
+ return toAjax(logininforService.deleteLogininforByIds(infoIds));
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+ @Log(title = "登录日志", businessType = BusinessType.CLEAN)
+ @DeleteMapping("/clean")
+ public AjaxResult clean()
+ {
+ logininforService.cleanLogininfor();
+ return success();
+ }
+
+ @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
+ @Log(title = "账户解锁", businessType = BusinessType.OTHER)
+ @GetMapping("/unlock/{userName}")
+ public AjaxResult unlock(@PathVariable("userName") String userName)
+ {
+ passwordService.clearLoginRecordCache(userName);
+ return success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
new file mode 100644
index 0000000..f056d65
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
@@ -0,0 +1,69 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysOperLog;
+import com.ruoyi.system.service.ISysOperLogService;
+
+/**
+ * 操作日志记录
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/operlog")
+public class SysOperlogController extends BaseController
+{
+ @Autowired
+ private ISysOperLogService operLogService;
+
+ @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysOperLog operLog)
+ {
+ startPage();
+ List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+ return getDataTable(list);
+ }
+
+ @Log(title = "操作日志", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysOperLog operLog)
+ {
+ List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+ ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
+ util.exportExcel(response, list, "操作日志");
+ }
+
+ @Log(title = "操作日志", businessType = BusinessType.DELETE)
+ @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+ @DeleteMapping("/{operIds}")
+ public AjaxResult remove(@PathVariable Long[] operIds)
+ {
+ return toAjax(operLogService.deleteOperLogByIds(operIds));
+ }
+
+ @Log(title = "操作日志", businessType = BusinessType.CLEAN)
+ @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+ @DeleteMapping("/clean")
+ public AjaxResult clean()
+ {
+ operLogService.cleanOperLog();
+ return success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
new file mode 100644
index 0000000..a442863
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
@@ -0,0 +1,83 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysUserOnline;
+import com.ruoyi.system.service.ISysUserOnlineService;
+
+/**
+ * 在线用户监控
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/online")
+public class SysUserOnlineController extends BaseController
+{
+ @Autowired
+ private ISysUserOnlineService userOnlineService;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @PreAuthorize("@ss.hasPermi('monitor:online:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(String ipaddr, String userName)
+ {
+ Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
+ List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
+ for (String key : keys)
+ {
+ LoginUser user = redisCache.getCacheObject(key);
+ if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
+ {
+ userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
+ }
+ else if (StringUtils.isNotEmpty(ipaddr))
+ {
+ userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
+ }
+ else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
+ {
+ userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
+ }
+ else
+ {
+ userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
+ }
+ }
+ Collections.reverse(userOnlineList);
+ userOnlineList.removeAll(Collections.singleton(null));
+ return getDataTable(userOnlineList);
+ }
+
+ /**
+ * 强退用户
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
+ @Log(title = "在线用户", businessType = BusinessType.FORCE)
+ @DeleteMapping("/{tokenId}")
+ public AjaxResult forceLogout(@PathVariable String tokenId)
+ {
+ redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
+ return success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java
new file mode 100644
index 0000000..91a854f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java
@@ -0,0 +1,133 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysConfig;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 参数配置 信息操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/config")
+public class SysConfigController extends BaseController
+{
+ @Autowired
+ private ISysConfigService configService;
+
+ /**
+ * 获取参数配置列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysConfig config)
+ {
+ startPage();
+ List<SysConfig> list = configService.selectConfigList(config);
+ return getDataTable(list);
+ }
+
+ @Log(title = "参数管理", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:config:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysConfig config)
+ {
+ List<SysConfig> list = configService.selectConfigList(config);
+ ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
+ util.exportExcel(response, list, "参数数据");
+ }
+
+ /**
+ * 根据参数编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:query')")
+ @GetMapping(value = "/{configId}")
+ public AjaxResult getInfo(@PathVariable Long configId)
+ {
+ return success(configService.selectConfigById(configId));
+ }
+
+ /**
+ * 根据参数键名查询参数值
+ */
+ @GetMapping(value = "/configKey/{configKey}")
+ public AjaxResult getConfigKey(@PathVariable String configKey)
+ {
+ return success(configService.selectConfigByKey(configKey));
+ }
+
+ /**
+ * 新增参数配置
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:add')")
+ @Log(title = "参数管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysConfig config)
+ {
+ if (!configService.checkConfigKeyUnique(config))
+ {
+ return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
+ }
+ config.setCreateBy(getUsername());
+ return toAjax(configService.insertConfig(config));
+ }
+
+ /**
+ * 修改参数配置
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:edit')")
+ @Log(title = "参数管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysConfig config)
+ {
+ if (!configService.checkConfigKeyUnique(config))
+ {
+ return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
+ }
+ config.setUpdateBy(getUsername());
+ return toAjax(configService.updateConfig(config));
+ }
+
+ /**
+ * 删除参数配置
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:remove')")
+ @Log(title = "参数管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{configIds}")
+ public AjaxResult remove(@PathVariable Long[] configIds)
+ {
+ configService.deleteConfigByIds(configIds);
+ return success();
+ }
+
+ /**
+ * 刷新参数缓存
+ */
+ @PreAuthorize("@ss.hasPermi('system:config:remove')")
+ @Log(title = "参数管理", businessType = BusinessType.CLEAN)
+ @DeleteMapping("/refreshCache")
+ public AjaxResult refreshCache()
+ {
+ configService.resetConfigCache();
+ return success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
new file mode 100644
index 0000000..59e7588
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
@@ -0,0 +1,132 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysDeptService;
+
+/**
+ * 部门信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dept")
+public class SysDeptController extends BaseController
+{
+ @Autowired
+ private ISysDeptService deptService;
+
+ /**
+ * 获取部门列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:list')")
+ @GetMapping("/list")
+ public AjaxResult list(SysDept dept)
+ {
+ List<SysDept> depts = deptService.selectDeptList(dept);
+ return success(depts);
+ }
+
+ /**
+ * 查询部门列表(排除节点)
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:list')")
+ @GetMapping("/list/exclude/{deptId}")
+ public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
+ {
+ List<SysDept> depts = deptService.selectDeptList(new SysDept());
+ depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
+ return success(depts);
+ }
+
+ /**
+ * 根据部门编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:query')")
+ @GetMapping(value = "/{deptId}")
+ public AjaxResult getInfo(@PathVariable Long deptId)
+ {
+ deptService.checkDeptDataScope(deptId);
+ return success(deptService.selectDeptById(deptId));
+ }
+
+ /**
+ * 新增部门
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:add')")
+ @Log(title = "部门管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysDept dept)
+ {
+ if (!deptService.checkDeptNameUnique(dept))
+ {
+ return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+ }
+ dept.setCreateBy(getUsername());
+ return toAjax(deptService.insertDept(dept));
+ }
+
+ /**
+ * 修改部门
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:edit')")
+ @Log(title = "部门管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysDept dept)
+ {
+ Long deptId = dept.getDeptId();
+ deptService.checkDeptDataScope(deptId);
+ if (!deptService.checkDeptNameUnique(dept))
+ {
+ return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+ }
+ else if (dept.getParentId().equals(deptId))
+ {
+ return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
+ }
+ else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
+ {
+ return error("该部门包含未停用的子部门!");
+ }
+ dept.setUpdateBy(getUsername());
+ return toAjax(deptService.updateDept(dept));
+ }
+
+ /**
+ * 删除部门
+ */
+ @PreAuthorize("@ss.hasPermi('system:dept:remove')")
+ @Log(title = "部门管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{deptId}")
+ public AjaxResult remove(@PathVariable Long deptId)
+ {
+ if (deptService.hasChildByDeptId(deptId))
+ {
+ return warn("存在下级部门,不允许删除");
+ }
+ if (deptService.checkDeptExistUser(deptId))
+ {
+ return warn("部门存在用户,不允许删除");
+ }
+ deptService.checkDeptDataScope(deptId);
+ return toAjax(deptService.deleteDeptById(deptId));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java
new file mode 100644
index 0000000..f65492b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java
@@ -0,0 +1,121 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDictDataService;
+import com.ruoyi.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/data")
+public class SysDictDataController extends BaseController
+{
+ @Autowired
+ private ISysDictDataService dictDataService;
+
+ @Autowired
+ private ISysDictTypeService dictTypeService;
+
+ @PreAuthorize("@ss.hasPermi('system:dict:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysDictData dictData)
+ {
+ startPage();
+ List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+ return getDataTable(list);
+ }
+
+ @Log(title = "字典数据", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:dict:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysDictData dictData)
+ {
+ List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+ ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
+ util.exportExcel(response, list, "字典数据");
+ }
+
+ /**
+ * 查询字典数据详细
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:query')")
+ @GetMapping(value = "/{dictCode}")
+ public AjaxResult getInfo(@PathVariable Long dictCode)
+ {
+ return success(dictDataService.selectDictDataById(dictCode));
+ }
+
+ /**
+ * 根据字典类型查询字典数据信息
+ */
+ @GetMapping(value = "/type/{dictType}")
+ public AjaxResult dictType(@PathVariable String dictType)
+ {
+ List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
+ if (StringUtils.isNull(data))
+ {
+ data = new ArrayList<SysDictData>();
+ }
+ return success(data);
+ }
+
+ /**
+ * 新增字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:add')")
+ @Log(title = "字典数据", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysDictData dict)
+ {
+ dict.setCreateBy(getUsername());
+ return toAjax(dictDataService.insertDictData(dict));
+ }
+
+ /**
+ * 修改保存字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+ @Log(title = "字典数据", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysDictData dict)
+ {
+ dict.setUpdateBy(getUsername());
+ return toAjax(dictDataService.updateDictData(dict));
+ }
+
+ /**
+ * 删除字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+ @Log(title = "字典类型", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{dictCodes}")
+ public AjaxResult remove(@PathVariable Long[] dictCodes)
+ {
+ dictDataService.deleteDictDataByIds(dictCodes);
+ return success();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java
new file mode 100644
index 0000000..3ff314c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java
@@ -0,0 +1,131 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDictType;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/type")
+public class SysDictTypeController extends BaseController
+{
+ @Autowired
+ private ISysDictTypeService dictTypeService;
+
+ @PreAuthorize("@ss.hasPermi('system:dict:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysDictType dictType)
+ {
+ startPage();
+ List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+ return getDataTable(list);
+ }
+
+ @Log(title = "字典类型", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:dict:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysDictType dictType)
+ {
+ List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+ ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
+ util.exportExcel(response, list, "字典类型");
+ }
+
+ /**
+ * 查询字典类型详细
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:query')")
+ @GetMapping(value = "/{dictId}")
+ public AjaxResult getInfo(@PathVariable Long dictId)
+ {
+ return success(dictTypeService.selectDictTypeById(dictId));
+ }
+
+ /**
+ * 新增字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:add')")
+ @Log(title = "字典类型", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysDictType dict)
+ {
+ if (!dictTypeService.checkDictTypeUnique(dict))
+ {
+ return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
+ }
+ dict.setCreateBy(getUsername());
+ return toAjax(dictTypeService.insertDictType(dict));
+ }
+
+ /**
+ * 修改字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+ @Log(title = "字典类型", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysDictType dict)
+ {
+ if (!dictTypeService.checkDictTypeUnique(dict))
+ {
+ return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
+ }
+ dict.setUpdateBy(getUsername());
+ return toAjax(dictTypeService.updateDictType(dict));
+ }
+
+ /**
+ * 删除字典类型
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+ @Log(title = "字典类型", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{dictIds}")
+ public AjaxResult remove(@PathVariable Long[] dictIds)
+ {
+ dictTypeService.deleteDictTypeByIds(dictIds);
+ return success();
+ }
+
+ /**
+ * 刷新字典缓存
+ */
+ @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+ @Log(title = "字典类型", businessType = BusinessType.CLEAN)
+ @DeleteMapping("/refreshCache")
+ public AjaxResult refreshCache()
+ {
+ dictTypeService.resetDictCache();
+ return success();
+ }
+
+ /**
+ * 获取字典选择框列表
+ */
+ @GetMapping("/optionselect")
+ public AjaxResult optionselect()
+ {
+ List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
+ return success(dictTypes);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java
new file mode 100644
index 0000000..13007eb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java
@@ -0,0 +1,29 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 首页
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysIndexController
+{
+ /** 系统基础配置 */
+ @Autowired
+ private RuoYiConfig ruoyiConfig;
+
+ /**
+ * 访问首页,提示语
+ */
+ @RequestMapping("/")
+ public String index()
+ {
+ return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
new file mode 100644
index 0000000..d959a17
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -0,0 +1,86 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginBody;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.web.service.SysLoginService;
+import com.ruoyi.framework.web.service.SysPermissionService;
+import com.ruoyi.system.service.ISysMenuService;
+
+/**
+ * 登录验证
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysLoginController
+{
+ @Autowired
+ private SysLoginService loginService;
+
+ @Autowired
+ private ISysMenuService menuService;
+
+ @Autowired
+ private SysPermissionService permissionService;
+
+ /**
+ * 登录方法
+ *
+ * @param loginBody 登录信息
+ * @return 结果
+ */
+ @PostMapping("/login")
+ public AjaxResult login(@RequestBody LoginBody loginBody)
+ {
+ AjaxResult ajax = AjaxResult.success();
+ // 生成令牌
+ String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
+ loginBody.getUuid());
+ ajax.put(Constants.TOKEN, token);
+ return ajax;
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @return 用户信息
+ */
+ @GetMapping("getInfo")
+ public AjaxResult getInfo()
+ {
+ SysUser user = SecurityUtils.getLoginUser().getUser();
+ // 角色集合
+ Set<String> roles = permissionService.getRolePermission(user);
+ // 权限集合
+ Set<String> permissions = permissionService.getMenuPermission(user);
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("user", user);
+ ajax.put("roles", roles);
+ ajax.put("permissions", permissions);
+ return ajax;
+ }
+
+ /**
+ * 获取路由信息
+ *
+ * @return 路由信息
+ */
+ @GetMapping("getRouters")
+ public AjaxResult getRouters()
+ {
+ Long userId = SecurityUtils.getUserId();
+ List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
+ return AjaxResult.success(menuService.buildMenus(menus));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java
new file mode 100644
index 0000000..03b6b65
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java
@@ -0,0 +1,142 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysMenuService;
+
+/**
+ * 菜单信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/menu")
+public class SysMenuController extends BaseController
+{
+ @Autowired
+ private ISysMenuService menuService;
+
+ /**
+ * 获取菜单列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:menu:list')")
+ @GetMapping("/list")
+ public AjaxResult list(SysMenu menu)
+ {
+ List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+ return success(menus);
+ }
+
+ /**
+ * 根据菜单编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:menu:query')")
+ @GetMapping(value = "/{menuId}")
+ public AjaxResult getInfo(@PathVariable Long menuId)
+ {
+ return success(menuService.selectMenuById(menuId));
+ }
+
+ /**
+ * 获取菜单下拉树列表
+ */
+ @GetMapping("/treeselect")
+ public AjaxResult treeselect(SysMenu menu)
+ {
+ List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+ return success(menuService.buildMenuTreeSelect(menus));
+ }
+
+ /**
+ * 加载对应角色菜单列表树
+ */
+ @GetMapping(value = "/roleMenuTreeselect/{roleId}")
+ public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
+ {
+ List<SysMenu> menus = menuService.selectMenuList(getUserId());
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
+ ajax.put("menus", menuService.buildMenuTreeSelect(menus));
+ return ajax;
+ }
+
+ /**
+ * 新增菜单
+ */
+ @PreAuthorize("@ss.hasPermi('system:menu:add')")
+ @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysMenu menu)
+ {
+ if (!menuService.checkMenuNameUnique(menu))
+ {
+ return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+ }
+ else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+ {
+ return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+ }
+ menu.setCreateBy(getUsername());
+ return toAjax(menuService.insertMenu(menu));
+ }
+
+ /**
+ * 修改菜单
+ */
+ @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+ @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysMenu menu)
+ {
+ if (!menuService.checkMenuNameUnique(menu))
+ {
+ return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+ }
+ else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+ {
+ return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+ }
+ else if (menu.getMenuId().equals(menu.getParentId()))
+ {
+ return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+ }
+ menu.setUpdateBy(getUsername());
+ return toAjax(menuService.updateMenu(menu));
+ }
+
+ /**
+ * 删除菜单
+ */
+ @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+ @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{menuId}")
+ public AjaxResult remove(@PathVariable("menuId") Long menuId)
+ {
+ if (menuService.hasChildByMenuId(menuId))
+ {
+ return warn("存在子菜单,不允许删除");
+ }
+ if (menuService.checkMenuExistRole(menuId))
+ {
+ return warn("菜单已分配,不允许删除");
+ }
+ return toAjax(menuService.deleteMenuById(menuId));
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java
new file mode 100644
index 0000000..8622828
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java
@@ -0,0 +1,91 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.SysNotice;
+import com.ruoyi.system.service.ISysNoticeService;
+
+/**
+ * 公告 信息操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/notice")
+public class SysNoticeController extends BaseController
+{
+ @Autowired
+ private ISysNoticeService noticeService;
+
+ /**
+ * 获取通知公告列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:notice:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysNotice notice)
+ {
+ startPage();
+ List<SysNotice> list = noticeService.selectNoticeList(notice);
+ return getDataTable(list);
+ }
+
+ /**
+ * 根据通知公告编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:notice:query')")
+ @GetMapping(value = "/{noticeId}")
+ public AjaxResult getInfo(@PathVariable Long noticeId)
+ {
+ return success(noticeService.selectNoticeById(noticeId));
+ }
+
+ /**
+ * 新增通知公告
+ */
+ @PreAuthorize("@ss.hasPermi('system:notice:add')")
+ @Log(title = "通知公告", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysNotice notice)
+ {
+ notice.setCreateBy(getUsername());
+ return toAjax(noticeService.insertNotice(notice));
+ }
+
+ /**
+ * 修改通知公告
+ */
+ @PreAuthorize("@ss.hasPermi('system:notice:edit')")
+ @Log(title = "通知公告", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysNotice notice)
+ {
+ notice.setUpdateBy(getUsername());
+ return toAjax(noticeService.updateNotice(notice));
+ }
+
+ /**
+ * 删除通知公告
+ */
+ @PreAuthorize("@ss.hasPermi('system:notice:remove')")
+ @Log(title = "通知公告", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{noticeIds}")
+ public AjaxResult remove(@PathVariable Long[] noticeIds)
+ {
+ return toAjax(noticeService.deleteNoticeByIds(noticeIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java
new file mode 100644
index 0000000..5b604cf
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java
@@ -0,0 +1,129 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysPost;
+import com.ruoyi.system.service.ISysPostService;
+
+/**
+ * 岗位信息操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/post")
+public class SysPostController extends BaseController
+{
+ @Autowired
+ private ISysPostService postService;
+
+ /**
+ * 获取岗位列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:post:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysPost post)
+ {
+ startPage();
+ List<SysPost> list = postService.selectPostList(post);
+ return getDataTable(list);
+ }
+
+ @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:post:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysPost post)
+ {
+ List<SysPost> list = postService.selectPostList(post);
+ ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
+ util.exportExcel(response, list, "岗位数据");
+ }
+
+ /**
+ * 根据岗位编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:post:query')")
+ @GetMapping(value = "/{postId}")
+ public AjaxResult getInfo(@PathVariable Long postId)
+ {
+ return success(postService.selectPostById(postId));
+ }
+
+ /**
+ * 新增岗位
+ */
+ @PreAuthorize("@ss.hasPermi('system:post:add')")
+ @Log(title = "岗位管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysPost post)
+ {
+ if (!postService.checkPostNameUnique(post))
+ {
+ return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+ }
+ else if (!postService.checkPostCodeUnique(post))
+ {
+ return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+ }
+ post.setCreateBy(getUsername());
+ return toAjax(postService.insertPost(post));
+ }
+
+ /**
+ * 修改岗位
+ */
+ @PreAuthorize("@ss.hasPermi('system:post:edit')")
+ @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysPost post)
+ {
+ if (!postService.checkPostNameUnique(post))
+ {
+ return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+ }
+ else if (!postService.checkPostCodeUnique(post))
+ {
+ return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+ }
+ post.setUpdateBy(getUsername());
+ return toAjax(postService.updatePost(post));
+ }
+
+ /**
+ * 删除岗位
+ */
+ @PreAuthorize("@ss.hasPermi('system:post:remove')")
+ @Log(title = "岗位管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{postIds}")
+ public AjaxResult remove(@PathVariable Long[] postIds)
+ {
+ return toAjax(postService.deletePostByIds(postIds));
+ }
+
+ /**
+ * 获取岗位选择框列表
+ */
+ @GetMapping("/optionselect")
+ public AjaxResult optionselect()
+ {
+ List<SysPost> posts = postService.selectPostAll();
+ return success(posts);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
new file mode 100644
index 0000000..d5faedd
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
@@ -0,0 +1,137 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.MimeTypeUtils;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 个人信息 业务处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user/profile")
+public class SysProfileController extends BaseController
+{
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private TokenService tokenService;
+
+ /**
+ * 个人信息
+ */
+ @GetMapping
+ public AjaxResult profile()
+ {
+ LoginUser loginUser = getLoginUser();
+ SysUser user = loginUser.getUser();
+ AjaxResult ajax = AjaxResult.success(user);
+ ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
+ ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
+ return ajax;
+ }
+
+ /**
+ * 修改用户
+ */
+ @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult updateProfile(@RequestBody SysUser user)
+ {
+ LoginUser loginUser = getLoginUser();
+ SysUser currentUser = loginUser.getUser();
+ currentUser.setNickName(user.getNickName());
+ currentUser.setEmail(user.getEmail());
+ currentUser.setPhonenumber(user.getPhonenumber());
+ currentUser.setSex(user.getSex());
+ if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
+ {
+ return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
+ }
+ if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
+ {
+ return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
+ }
+ if (userService.updateUserProfile(currentUser) > 0)
+ {
+ // 更新缓存用户信息
+ tokenService.setLoginUser(loginUser);
+ return success();
+ }
+ return error("修改个人信息异常,请联系管理员");
+ }
+
+ /**
+ * 重置密码
+ */
+ @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+ @PutMapping("/updatePwd")
+ public AjaxResult updatePwd(String oldPassword, String newPassword)
+ {
+ LoginUser loginUser = getLoginUser();
+ String userName = loginUser.getUsername();
+ String password = loginUser.getPassword();
+ if (!SecurityUtils.matchesPassword(oldPassword, password))
+ {
+ return error("修改密码失败,旧密码错误");
+ }
+ if (SecurityUtils.matchesPassword(newPassword, password))
+ {
+ return error("新密码不能与旧密码相同");
+ }
+ newPassword = SecurityUtils.encryptPassword(newPassword);
+ if (userService.resetUserPwd(userName, newPassword) > 0)
+ {
+ // 更新缓存用户密码
+ loginUser.getUser().setPassword(newPassword);
+ tokenService.setLoginUser(loginUser);
+ return success();
+ }
+ return error("修改密码异常,请联系管理员");
+ }
+
+ /**
+ * 头像上传
+ */
+ @Log(title = "用户头像", businessType = BusinessType.UPDATE)
+ @PostMapping("/avatar")
+ public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
+ {
+ if (!file.isEmpty())
+ {
+ LoginUser loginUser = getLoginUser();
+ String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
+ if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
+ {
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("imgUrl", avatar);
+ // 更新缓存用户头像
+ loginUser.getUser().setAvatar(avatar);
+ tokenService.setLoginUser(loginUser);
+ return ajax;
+ }
+ }
+ return error("上传图片异常,请联系管理员");
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
new file mode 100644
index 0000000..fe19249
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
@@ -0,0 +1,38 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.RegisterBody;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.web.service.SysRegisterService;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 注册验证
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysRegisterController extends BaseController
+{
+ @Autowired
+ private SysRegisterService registerService;
+
+ @Autowired
+ private ISysConfigService configService;
+
+ @PostMapping("/register")
+ public AjaxResult register(@RequestBody RegisterBody user)
+ {
+ if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
+ {
+ return error("当前系统没有开启注册功能!");
+ }
+ String msg = registerService.register(user);
+ return StringUtils.isEmpty(msg) ? success() : error(msg);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
new file mode 100644
index 0000000..c8e7ab6
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
@@ -0,0 +1,262 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.framework.web.service.SysPermissionService;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.domain.SysUserRole;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 角色信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/role")
+public class SysRoleController extends BaseController
+{
+ @Autowired
+ private ISysRoleService roleService;
+
+ @Autowired
+ private TokenService tokenService;
+
+ @Autowired
+ private SysPermissionService permissionService;
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysDeptService deptService;
+
+ @PreAuthorize("@ss.hasPermi('system:role:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysRole role)
+ {
+ startPage();
+ List<SysRole> list = roleService.selectRoleList(role);
+ return getDataTable(list);
+ }
+
+ @Log(title = "角色管理", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:role:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysRole role)
+ {
+ List<SysRole> list = roleService.selectRoleList(role);
+ ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
+ util.exportExcel(response, list, "角色数据");
+ }
+
+ /**
+ * 根据角色编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:query')")
+ @GetMapping(value = "/{roleId}")
+ public AjaxResult getInfo(@PathVariable Long roleId)
+ {
+ roleService.checkRoleDataScope(roleId);
+ return success(roleService.selectRoleById(roleId));
+ }
+
+ /**
+ * 新增角色
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:add')")
+ @Log(title = "角色管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysRole role)
+ {
+ if (!roleService.checkRoleNameUnique(role))
+ {
+ return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
+ }
+ else if (!roleService.checkRoleKeyUnique(role))
+ {
+ return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
+ }
+ role.setCreateBy(getUsername());
+ return toAjax(roleService.insertRole(role));
+
+ }
+
+ /**
+ * 修改保存角色
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysRole role)
+ {
+ roleService.checkRoleAllowed(role);
+ roleService.checkRoleDataScope(role.getRoleId());
+ if (!roleService.checkRoleNameUnique(role))
+ {
+ return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
+ }
+ else if (!roleService.checkRoleKeyUnique(role))
+ {
+ return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
+ }
+ role.setUpdateBy(getUsername());
+
+ if (roleService.updateRole(role) > 0)
+ {
+ // 更新缓存用户权限
+ LoginUser loginUser = getLoginUser();
+ if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
+ {
+ loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
+ loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
+ tokenService.setLoginUser(loginUser);
+ }
+ return success();
+ }
+ return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
+ }
+
+ /**
+ * 修改保存数据权限
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+ @PutMapping("/dataScope")
+ public AjaxResult dataScope(@RequestBody SysRole role)
+ {
+ roleService.checkRoleAllowed(role);
+ roleService.checkRoleDataScope(role.getRoleId());
+ return toAjax(roleService.authDataScope(role));
+ }
+
+ /**
+ * 状态修改
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+ @PutMapping("/changeStatus")
+ public AjaxResult changeStatus(@RequestBody SysRole role)
+ {
+ roleService.checkRoleAllowed(role);
+ roleService.checkRoleDataScope(role.getRoleId());
+ role.setUpdateBy(getUsername());
+ return toAjax(roleService.updateRoleStatus(role));
+ }
+
+ /**
+ * 删除角色
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:remove')")
+ @Log(title = "角色管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{roleIds}")
+ public AjaxResult remove(@PathVariable Long[] roleIds)
+ {
+ return toAjax(roleService.deleteRoleByIds(roleIds));
+ }
+
+ /**
+ * 获取角色选择框列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:query')")
+ @GetMapping("/optionselect")
+ public AjaxResult optionselect()
+ {
+ return success(roleService.selectRoleAll());
+ }
+
+ /**
+ * 查询已分配用户角色列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:list')")
+ @GetMapping("/authUser/allocatedList")
+ public TableDataInfo allocatedList(SysUser user)
+ {
+ startPage();
+ List<SysUser> list = userService.selectAllocatedList(user);
+ return getDataTable(list);
+ }
+
+ /**
+ * 查询未分配用户角色列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:list')")
+ @GetMapping("/authUser/unallocatedList")
+ public TableDataInfo unallocatedList(SysUser user)
+ {
+ startPage();
+ List<SysUser> list = userService.selectUnallocatedList(user);
+ return getDataTable(list);
+ }
+
+ /**
+ * 取消授权用户
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.GRANT)
+ @PutMapping("/authUser/cancel")
+ public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
+ {
+ return toAjax(roleService.deleteAuthUser(userRole));
+ }
+
+ /**
+ * 批量取消授权用户
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.GRANT)
+ @PutMapping("/authUser/cancelAll")
+ public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
+ {
+ return toAjax(roleService.deleteAuthUsers(roleId, userIds));
+ }
+
+ /**
+ * 批量选择用户授权
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:edit')")
+ @Log(title = "角色管理", businessType = BusinessType.GRANT)
+ @PutMapping("/authUser/selectAll")
+ public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
+ {
+ roleService.checkRoleDataScope(roleId);
+ return toAjax(roleService.insertAuthUsers(roleId, userIds));
+ }
+
+ /**
+ * 获取对应角色部门树列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:role:query')")
+ @GetMapping(value = "/deptTree/{roleId}")
+ public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
+ {
+ AjaxResult ajax = AjaxResult.success();
+ ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
+ ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
+ return ajax;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
new file mode 100644
index 0000000..96bd559
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
@@ -0,0 +1,256 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.system.service.ISysPostService;
+import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 用户信息
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user")
+public class SysUserController extends BaseController
+{
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysRoleService roleService;
+
+ @Autowired
+ private ISysDeptService deptService;
+
+ @Autowired
+ private ISysPostService postService;
+
+ /**
+ * 获取用户列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysUser user)
+ {
+ startPage();
+ List<SysUser> list = userService.selectUserList(user);
+ return getDataTable(list);
+ }
+
+ @Log(title = "用户管理", businessType = BusinessType.EXPORT)
+ @PreAuthorize("@ss.hasPermi('system:user:export')")
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysUser user)
+ {
+ List<SysUser> list = userService.selectUserList(user);
+ ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+ util.exportExcel(response, list, "用户数据");
+ }
+
+ @Log(title = "用户管理", businessType = BusinessType.IMPORT)
+ @PreAuthorize("@ss.hasPermi('system:user:import')")
+ @PostMapping("/importData")
+ public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
+ {
+ ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+ List<SysUser> userList = util.importExcel(file.getInputStream());
+ String operName = getUsername();
+ String message = userService.importUser(userList, updateSupport, operName);
+ return success(message);
+ }
+
+ @PostMapping("/importTemplate")
+ public void importTemplate(HttpServletResponse response)
+ {
+ ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+ util.importTemplateExcel(response, "用户数据");
+ }
+
+ /**
+ * 根据用户编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:query')")
+ @GetMapping(value = { "/", "/{userId}" })
+ public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
+ {
+ userService.checkUserDataScope(userId);
+ AjaxResult ajax = AjaxResult.success();
+ List<SysRole> roles = roleService.selectRoleAll();
+ ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+ ajax.put("posts", postService.selectPostAll());
+ if (StringUtils.isNotNull(userId))
+ {
+ SysUser sysUser = userService.selectUserById(userId);
+ ajax.put(AjaxResult.DATA_TAG, sysUser);
+ ajax.put("postIds", postService.selectPostListByUserId(userId));
+ ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
+ }
+ return ajax;
+ }
+
+ /**
+ * 新增用户
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:add')")
+ @Log(title = "用户管理", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@Validated @RequestBody SysUser user)
+ {
+ deptService.checkDeptDataScope(user.getDeptId());
+ roleService.checkRoleDataScope(user.getRoleIds());
+ if (!userService.checkUserNameUnique(user))
+ {
+ return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
+ }
+ else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
+ {
+ return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
+ }
+ else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
+ {
+ return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+ }
+ user.setCreateBy(getUsername());
+ user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+ return toAjax(userService.insertUser(user));
+ }
+
+ /**
+ * 修改用户
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:edit')")
+ @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@Validated @RequestBody SysUser user)
+ {
+ userService.checkUserAllowed(user);
+ userService.checkUserDataScope(user.getUserId());
+ deptService.checkDeptDataScope(user.getDeptId());
+ roleService.checkRoleDataScope(user.getRoleIds());
+ if (!userService.checkUserNameUnique(user))
+ {
+ return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
+ }
+ else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
+ {
+ return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
+ }
+ else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
+ {
+ return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+ }
+ user.setUpdateBy(getUsername());
+ return toAjax(userService.updateUser(user));
+ }
+
+ /**
+ * 删除用户
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:remove')")
+ @Log(title = "用户管理", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{userIds}")
+ public AjaxResult remove(@PathVariable Long[] userIds)
+ {
+ if (ArrayUtils.contains(userIds, getUserId()))
+ {
+ return error("当前用户不能删除");
+ }
+ return toAjax(userService.deleteUserByIds(userIds));
+ }
+
+ /**
+ * 重置密码
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
+ @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+ @PutMapping("/resetPwd")
+ public AjaxResult resetPwd(@RequestBody SysUser user)
+ {
+ userService.checkUserAllowed(user);
+ userService.checkUserDataScope(user.getUserId());
+ user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+ user.setUpdateBy(getUsername());
+ return toAjax(userService.resetPwd(user));
+ }
+
+ /**
+ * 状态修改
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:edit')")
+ @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+ @PutMapping("/changeStatus")
+ public AjaxResult changeStatus(@RequestBody SysUser user)
+ {
+ userService.checkUserAllowed(user);
+ userService.checkUserDataScope(user.getUserId());
+ user.setUpdateBy(getUsername());
+ return toAjax(userService.updateUserStatus(user));
+ }
+
+ /**
+ * 根据用户编号获取授权角色
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:query')")
+ @GetMapping("/authRole/{userId}")
+ public AjaxResult authRole(@PathVariable("userId") Long userId)
+ {
+ AjaxResult ajax = AjaxResult.success();
+ SysUser user = userService.selectUserById(userId);
+ List<SysRole> roles = roleService.selectRolesByUserId(userId);
+ ajax.put("user", user);
+ ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+ return ajax;
+ }
+
+ /**
+ * 用户授权角色
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:edit')")
+ @Log(title = "用户管理", businessType = BusinessType.GRANT)
+ @PutMapping("/authRole")
+ public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
+ {
+ userService.checkUserDataScope(userId);
+ roleService.checkRoleDataScope(roleIds);
+ userService.insertUserAuth(userId, roleIds);
+ return success();
+ }
+
+ /**
+ * 获取部门树列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:user:list')")
+ @GetMapping("/deptTree")
+ public AjaxResult deptTree(SysDept dept)
+ {
+ return success(deptService.selectDeptTreeList(dept));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java
new file mode 100644
index 0000000..586e1b8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java
@@ -0,0 +1,175 @@
+package com.ruoyi.web.controller.tool;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.utils.StringUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+/**
+ * swagger 用户测试方法
+ *
+ * @author ruoyi
+ */
+@Tag(name = "用户信息管理")
+@RestController
+@RequestMapping("/test/user")
+public class TestController extends BaseController
+{
+ private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
+ {
+ users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
+ users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
+ }
+
+ @Operation(summary = "获取用户列表")
+ @GetMapping("/list")
+ public R<List<UserEntity>> userList()
+ {
+ List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
+ return R.ok(userList);
+ }
+
+ @Operation(summary = "获取用户详细")
+ @GetMapping("/{userId}")
+ public R<UserEntity> getUser(@PathVariable(name = "userId")
+ Integer userId)
+ {
+ if (!users.isEmpty() && users.containsKey(userId))
+ {
+ return R.ok(users.get(userId));
+ }
+ else
+ {
+ return R.fail("用户不存在");
+ }
+ }
+
+ @Operation(summary = "新增用户")
+ @PostMapping("/save")
+ public R<String> save(UserEntity user)
+ {
+ if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+ {
+ return R.fail("用户ID不能为空");
+ }
+ users.put(user.getUserId(), user);
+ return R.ok();
+ }
+
+ @Operation(summary = "更新用户")
+ @PutMapping("/update")
+ public R<String> update(@RequestBody
+ UserEntity user)
+ {
+ if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+ {
+ return R.fail("用户ID不能为空");
+ }
+ if (users.isEmpty() || !users.containsKey(user.getUserId()))
+ {
+ return R.fail("用户不存在");
+ }
+ users.remove(user.getUserId());
+ users.put(user.getUserId(), user);
+ return R.ok();
+ }
+
+ @Operation(summary = "删除用户信息")
+ @DeleteMapping("/{userId}")
+ public R<String> delete(@PathVariable(name = "userId")
+ Integer userId)
+ {
+ if (!users.isEmpty() && users.containsKey(userId))
+ {
+ users.remove(userId);
+ return R.ok();
+ }
+ else
+ {
+ return R.fail("用户不存在");
+ }
+ }
+}
+
+@Schema(description = "用户实体")
+class UserEntity
+{
+ @Schema(title = "用户ID")
+ private Integer userId;
+
+ @Schema(title = "用户名称")
+ private String username;
+
+ @Schema(title = "用户密码")
+ private String password;
+
+ @Schema(title = "用户手机")
+ private String mobile;
+
+ public UserEntity()
+ {
+
+ }
+
+ public UserEntity(Integer userId, String username, String password, String mobile)
+ {
+ this.userId = userId;
+ this.username = username;
+ this.password = password;
+ this.mobile = mobile;
+ }
+
+ public Integer getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Integer userId)
+ {
+ this.userId = userId;
+ }
+
+ public String getUsername()
+ {
+ return username;
+ }
+
+ public void setUsername(String username)
+ {
+ this.username = username;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public void setPassword(String password)
+ {
+ this.password = password;
+ }
+
+ public String getMobile()
+ {
+ return mobile;
+ }
+
+ public void setMobile(String mobile)
+ {
+ this.mobile = mobile;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java
new file mode 100644
index 0000000..9ab7c24
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java
@@ -0,0 +1,64 @@
+package com.ruoyi.web.core.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.ruoyi.common.config.RuoYiConfig;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+
+/**
+ * Swagger2的接口配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class SwaggerConfig
+{
+ /** 系统基础配置 */
+ @Autowired
+ private RuoYiConfig ruoyiConfig;
+
+ /**
+ * 自定义的 OpenAPI 对象
+ */
+ @Bean
+ public OpenAPI customOpenApi()
+ {
+ return new OpenAPI().components(new Components()
+ // 设置认证的请求头
+ .addSecuritySchemes("apikey", securityScheme()))
+ .addSecurityItem(new SecurityRequirement().addList("apikey"))
+ .info(getApiInfo());
+ }
+
+ @Bean
+ public SecurityScheme securityScheme()
+ {
+ return new SecurityScheme()
+ .type(SecurityScheme.Type.APIKEY)
+ .name("Authorization")
+ .in(SecurityScheme.In.HEADER)
+ .scheme("Bearer");
+ }
+
+ /**
+ * 添加摘要信息
+ */
+ public Info getApiInfo()
+ {
+ return new Info()
+ // 设置标题
+ .title("标题:若依管理系统_接口文档")
+ // 描述
+ .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+ // 作者信息
+ .contact(new Contact().name(ruoyiConfig.getName()))
+ // 版本
+ .version("版本号:" + ruoyiConfig.getVersion());
+ }
+}
diff --git a/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties
new file mode 100644
index 0000000..37e7b58
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties
@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson2.*.jar
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml
new file mode 100644
index 0000000..ffe92e5
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/application-druid.yml
@@ -0,0 +1,61 @@
+# 数据源配置
+spring:
+ datasource:
+ type: com.alibaba.druid.pool.DruidDataSource
+ driverClassName: com.mysql.cj.jdbc.Driver
+ druid:
+ # 主库数据源
+ master:
+ url: jdbc:mysql://rm-bp1v264vgxpa95ferbo.mysql.rds.aliyuncs.com:3306/jrx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: root
+ password: Jrx5201314
+ # 从库数据源
+ slave:
+ # 从数据源开关/默认关闭
+ enabled: false
+ url:
+ username:
+ password:
+ # 初始连接数
+ initialSize: 5
+ # 最小连接池数量
+ minIdle: 10
+ # 最大连接池数量
+ maxActive: 20
+ # 配置获取连接等待超时的时间
+ maxWait: 60000
+ # 配置连接超时时间
+ connectTimeout: 30000
+ # 配置网络超时时间
+ socketTimeout: 60000
+ # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+ timeBetweenEvictionRunsMillis: 60000
+ # 配置一个连接在池中最小生存的时间,单位是毫秒
+ minEvictableIdleTimeMillis: 300000
+ # 配置一个连接在池中最大生存的时间,单位是毫秒
+ maxEvictableIdleTimeMillis: 900000
+ # 配置检测连接是否有效
+ validationQuery: SELECT 1 FROM DUAL
+ testWhileIdle: true
+ testOnBorrow: false
+ testOnReturn: false
+ webStatFilter:
+ enabled: true
+ statViewServlet:
+ enabled: true
+ # 设置白名单,不填则允许所有访问
+ allow:
+ url-pattern: /druid/*
+ # 控制台管理用户名和密码
+ login-username: ruoyi
+ login-password: 123456
+ filter:
+ stat:
+ enabled: true
+ # 慢SQL记录
+ log-slow-sql: true
+ slow-sql-millis: 1000
+ merge-sql: true
+ wall:
+ config:
+ multi-statement-allow: true
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
new file mode 100644
index 0000000..0333eba
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -0,0 +1,170 @@
+# 项目相关配置
+ruoyi:
+ # 名称
+ name: TdHub
+ # 版本
+ version: 3.8.8
+ # 版权年份
+ copyrightYear: 2024
+ profile: ./download
+ # 获取ip地址开关
+ addressEnabled: false
+ # 验证码类型 math 数字计算 char 字符验证
+ captchaType: math
+
+# 开发环境配置
+server:
+ # 服务器的HTTP端口,默认为8080
+ port: 8080
+ servlet:
+ # 应用的访问路径
+ context-path: /
+ tomcat:
+ # tomcat的URI编码
+ uri-encoding: UTF-8
+ # 连接数满后的排队数,默认为100
+ accept-count: 1000
+ threads:
+ # tomcat最大线程数,默认为200
+ max: 800
+ # Tomcat启动初始化的线程数,默认值10
+ min-spare: 100
+
+# 日志配置
+logging:
+ level:
+ com.ruoyi: debug
+ org.springframework: warn
+
+# 用户配置
+user:
+ password:
+ # 密码最大错误次数
+ maxRetryCount: 5
+ # 密码锁定时间(默认10分钟)
+ lockTime: 10
+
+# Spring配置
+spring:
+ # 资源信息
+ messages:
+ # 国际化资源文件路径
+ basename: i18n/messages
+ profiles:
+ active: druid
+ # 文件上传
+ servlet:
+ multipart:
+ # 单个文件大小
+ max-file-size: 10MB
+ # 设置总上传的文件大小
+ max-request-size: 20MB
+ # 服务模块
+ devtools:
+ restart:
+ # 热部署开关
+ enabled: true
+ data:
+ # redis 配置
+ redis:
+ # 地址
+ host: localhost
+ # 端口,默认为6379
+ port: 6379
+ # 数据库索引
+ database: 0
+ # 密码
+ password:
+ # 连接超时时间
+ timeout: 10s
+ lettuce:
+ pool:
+ # 连接池中的最小空闲连接
+ min-idle: 0
+ # 连接池中的最大空闲连接
+ max-idle: 8
+ # 连接池的最大数据库连接数
+ max-active: 8
+ # #连接池最大阻塞等待时间(使用负值表示没有限制)
+ max-wait: -1ms
+
+# token配置
+token:
+ # 令牌自定义标识
+ header: Authorization
+ # 令牌密钥
+ secret: abcdefghijklmnopqrstuvwxyz
+ # 令牌有效期(默认30分钟)
+ expireTime: 30
+
+# MyBatis配置
+mybatis:
+ # 搜索指定包别名
+ typeAliasesPackage: com.ruoyi.**.domain
+ # 配置mapper的扫描,找到所有的mapper.xml映射文件
+ mapperLocations: classpath*:mapper/**/*Mapper.xml
+ # 加载全局的配置文件
+ configLocation: classpath:mybatis/mybatis-config.xml
+
+# MyBatis-plus配置
+mybatis-plus:
+ # 搜索指定包别名
+ typeAliasesPackage: com.ruoyi.**.domain
+ # 配置mapper的扫描,找到所有的mapper.xml映射文件
+ mapperLocations: classpath:mapper/*.xml
+ configLocation: classpath:mybatis/mybatis-config.xml
+ # 全局配置
+ global-config:
+ # 主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
+ id-type: 0
+ # 字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
+ field-strategy: 2
+ # 驼峰下划线转换
+ db-column-underline: true
+ # 刷新mapper 调试神器
+ refresh-mapper: true
+ # 配置
+ configuration:
+ # 驼峰式命名
+ map-underscore-to-camel-case: true
+ # 全局映射器启用缓存
+ cache-enabled: true
+ # 指定 MyBatis 所用日志的具体实现
+ log-impl: org.slf4j;
+ # 配置默认的执行器
+ default-executor-type: reuse
+ # 允许 JDBC 支持自动生成主键
+ use-generated-keys: true
+
+
+# PageHelper分页插件
+pagehelper:
+ helperDialect: mysql
+ supportMethodsArguments: true
+ params: count=countSql
+
+# Springdoc配置
+springdoc:
+ api-docs:
+ path: /v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+ tags-sorter: alpha
+ group-configs:
+ - group: 'default'
+ display-name: '测试模块'
+ paths-to-match: '/**'
+ packages-to-scan: com.ruoyi.web.controller.tool
+
+# 防止XSS攻击
+xss:
+ # 过滤开关
+ enabled: true
+ # 排除链接(多个用逗号分隔)
+ excludes: /system/notice
+ # 匹配链接
+ urlPatterns: /system/*,/monitor/*,/tool/*
+
+# 设置是否调试,调试情况可跳过接口认证
+debug: false
diff --git a/ruoyi-admin/src/main/resources/banner.txt b/ruoyi-admin/src/main/resources/banner.txt
new file mode 100644
index 0000000..44b7519
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/banner.txt
@@ -0,0 +1,24 @@
+Application Version: ${ruoyi.version}
+Spring Boot Version: ${spring-boot.version}
+////////////////////////////////////////////////////////////////////
+// _ooOoo_ //
+// o8888888o //
+// 88" . "88 //
+// (| ^_^ |) //
+// O\ = /O //
+// ____/`---'\____ //
+// .' \\| |// `. //
+// / \\||| : |||// \ //
+// / _||||| -:- |||||- \ //
+// | | \\\ - /// | | //
+// | \_| ''\---/'' | | //
+// \ .-\__ `-` ___/-. / //
+// ___`. .' /--.--\ `. . ___ //
+// ."" '< `.___\_<|>_/___.' >'"". //
+// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
+// \ \ `-. \_ __\ /__ _/ .-` / / //
+// ========`-.____`-.___\_____/___.-`____.-'======== //
+// `=---=' //
+// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
+// 贾仁香香香香香香香香香香香香香香香香香香香香香香 //
+////////////////////////////////////////////////////////////////////
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties
new file mode 100644
index 0000000..93de005
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/i18n/messages.properties
@@ -0,0 +1,38 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+login.blocked=很遗憾,访问IP已被列入系统黑名单
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml
new file mode 100644
index 0000000..a360583
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/logback.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+ <!-- 日志存放路径 -->
+ <property name="log.path" value="/home/ruoyi/logs" />
+ <!-- 日志输出格式 -->
+ <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+ <!-- 控制台输出 -->
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ </appender>
+
+ <!-- 系统日志输出 -->
+ <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/sys-info.log</file>
+ <!-- 循环政策:基于时间创建日志文件 -->
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 日志文件名格式 -->
+ <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 日志最大的历史 60天 -->
+ <maxHistory>60</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <!-- 过滤的级别 -->
+ <level>INFO</level>
+ <!-- 匹配时的操作:接收(记录) -->
+ <onMatch>ACCEPT</onMatch>
+ <!-- 不匹配时的操作:拒绝(不记录) -->
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+
+ <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/sys-error.log</file>
+ <!-- 循环政策:基于时间创建日志文件 -->
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 日志文件名格式 -->
+ <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 日志最大的历史 60天 -->
+ <maxHistory>60</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <!-- 过滤的级别 -->
+ <level>ERROR</level>
+ <!-- 匹配时的操作:接收(记录) -->
+ <onMatch>ACCEPT</onMatch>
+ <!-- 不匹配时的操作:拒绝(不记录) -->
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+
+ <!-- 用户访问日志输出 -->
+ <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${log.path}/sys-user.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!-- 按天回滚 daily -->
+ <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!-- 日志最大的历史 60天 -->
+ <maxHistory>60</maxHistory>
+ </rollingPolicy>
+ <encoder>
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ </appender>
+
+ <!-- 系统模块日志级别控制 -->
+ <logger name="com.ruoyi" level="info" />
+ <!-- Spring日志级别控制 -->
+ <logger name="org.springframework" level="warn" />
+
+ <root level="info">
+ <appender-ref ref="console" />
+ </root>
+
+ <!--系统操作日志-->
+ <root level="info">
+ <appender-ref ref="file_info" />
+ <appender-ref ref="file_error" />
+ </root>
+
+ <!--系统用户操作日志-->
+ <logger name="sys-user" level="info">
+ <appender-ref ref="sys-user"/>
+ </logger>
+</configuration>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml
new file mode 100644
index 0000000..ac47c03
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+ <!-- 全局参数 -->
+ <settings>
+ <!-- 使全局的映射器启用或禁用缓存 -->
+ <setting name="cacheEnabled" value="true" />
+ <!-- 允许JDBC 支持自动生成主键 -->
+ <setting name="useGeneratedKeys" value="true" />
+ <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+ <setting name="defaultExecutorType" value="SIMPLE" />
+ <!-- 指定 MyBatis 所用日志的具体实现 -->
+ <setting name="logImpl" value="SLF4J" />
+ <!-- 使用驼峰命名法转换字段 -->
+ <!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+ </settings>
+
+</configuration>
diff --git a/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT-shaded.jar b/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT-shaded.jar
new file mode 100644
index 0000000..495ea55
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT-shaded.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT.jar b/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..ce5fa19
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-cli-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-client.jar b/ruoyi-admin/torrent/ttorrent-client.jar
new file mode 100644
index 0000000..f84d28c
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-client.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-common-1.3.0-SNAPSHOT.jar b/ruoyi-admin/torrent/ttorrent-common-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..56f922e
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-common-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-test-api-1.0.jar b/ruoyi-admin/torrent/ttorrent-test-api-1.0.jar
new file mode 100644
index 0000000..27545ad
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-test-api-1.0.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-tests-1.3.0-SNAPSHOT.jar b/ruoyi-admin/torrent/ttorrent-tests-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..b5b2ccd
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-tests-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/ruoyi-admin/torrent/ttorrent-tracker.jar b/ruoyi-admin/torrent/ttorrent-tracker.jar
new file mode 100644
index 0000000..1309a9f
--- /dev/null
+++ b/ruoyi-admin/torrent/ttorrent-tracker.jar
Binary files differ
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
new file mode 100644
index 0000000..2b8c2c0
--- /dev/null
+++ b/ruoyi-common/pom.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-common</artifactId>
+
+ <description>
+ common通用工具
+ </description>
+
+ <dependencies>
+
+ <!-- Spring框架基本的核心工具 -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ </dependency>
+
+ <!-- SpringWeb模块 -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ </dependency>
+
+ <!-- spring security 安全认证 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
+
+ <!-- pagehelper 分页插件 -->
+ <dependency>
+ <groupId>com.github.pagehelper</groupId>
+ <artifactId>pagehelper-spring-boot-starter</artifactId>
+ </dependency>
+
+ <!-- 自定义验证注解 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+
+ <!--常用工具类 -->
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+
+ <!-- JSON工具类 -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+
+ <!-- 阿里JSON解析器 -->
+ <dependency>
+ <groupId>com.alibaba.fastjson2</groupId>
+ <artifactId>fastjson2</artifactId>
+ </dependency>
+
+ <!-- io常用工具类 -->
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+
+ <!-- excel工具 -->
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml</artifactId>
+ </dependency>
+
+ <!-- yml解析器 -->
+ <dependency>
+ <groupId>org.yaml</groupId>
+ <artifactId>snakeyaml</artifactId>
+ </dependency>
+
+ <!-- Token生成与解析-->
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt</artifactId>
+ </dependency>
+
+ <!-- Jaxb -->
+ <dependency>
+ <groupId>javax.xml.bind</groupId>
+ <artifactId>jaxb-api</artifactId>
+ </dependency>
+
+ <!-- redis 缓存操作 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-redis</artifactId>
+ </dependency>
+
+ <!-- pool 对象池 -->
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-pool2</artifactId>
+ </dependency>
+
+ <!-- 解析客户端操作系统、浏览器等 -->
+ <dependency>
+ <groupId>eu.bitwalker</groupId>
+ <artifactId>UserAgentUtils</artifactId>
+ </dependency>
+
+ <!-- servlet包 -->
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java
new file mode 100644
index 0000000..1d6d4f4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java
@@ -0,0 +1,19 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 匿名访问不鉴权注解
+ *
+ * @author ruoyi
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Anonymous
+{
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java
new file mode 100644
index 0000000..be49c80
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java
@@ -0,0 +1,33 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 数据权限过滤注解
+ *
+ * @author ruoyi
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataScope
+{
+ /**
+ * 部门表的别名
+ */
+ public String deptAlias() default "";
+
+ /**
+ * 用户表的别名
+ */
+ public String userAlias() default "";
+
+ /**
+ * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
+ */
+ public String permission() default "";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java
new file mode 100644
index 0000000..79cd191
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java
@@ -0,0 +1,28 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.ruoyi.common.enums.DataSourceType;
+
+/**
+ * 自定义多数据源切换注解
+ *
+ * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
+ *
+ * @author ruoyi
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataSource
+{
+ /**
+ * 切换数据源名称
+ */
+ public DataSourceType value() default DataSourceType.MASTER;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java
new file mode 100644
index 0000000..27e587d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java
@@ -0,0 +1,192 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.math.BigDecimal;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import com.ruoyi.common.utils.poi.ExcelHandlerAdapter;
+
+/**
+ * 自定义导出Excel数据注解
+ *
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Excel
+{
+ /**
+ * 导出时在excel中排序
+ */
+ public int sort() default Integer.MAX_VALUE;
+
+ /**
+ * 导出到Excel中的名字.
+ */
+ public String name() default "";
+
+ /**
+ * 日期格式, 如: yyyy-MM-dd
+ */
+ public String dateFormat() default "";
+
+ /**
+ * 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
+ */
+ public String dictType() default "";
+
+ /**
+ * 读取内容转表达式 (如: 0=男,1=女,2=未知)
+ */
+ public String readConverterExp() default "";
+
+ /**
+ * 分隔符,读取字符串组内容
+ */
+ public String separator() default ",";
+
+ /**
+ * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
+ */
+ public int scale() default -1;
+
+ /**
+ * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
+ */
+ public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
+
+ /**
+ * 导出时在excel中每个列的高度
+ */
+ public double height() default 14;
+
+ /**
+ * 导出时在excel中每个列的宽度
+ */
+ public double width() default 16;
+
+ /**
+ * 文字后缀,如% 90 变成90%
+ */
+ public String suffix() default "";
+
+ /**
+ * 当值为空时,字段的默认值
+ */
+ public String defaultValue() default "";
+
+ /**
+ * 提示信息
+ */
+ public String prompt() default "";
+
+ /**
+ * 设置只能选择不能输入的列内容.
+ */
+ public String[] combo() default {};
+
+ /**
+ * 是否从字典读数据到combo,默认不读取,如读取需要设置dictType注解.
+ */
+ public boolean comboReadDict() default false;
+
+ /**
+ * 是否需要纵向合并单元格,应对需求:含有list集合单元格)
+ */
+ public boolean needMerge() default false;
+
+ /**
+ * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
+ */
+ public boolean isExport() default true;
+
+ /**
+ * 另一个类中的属性名称,支持多级获取,以小数点隔开
+ */
+ public String targetAttr() default "";
+
+ /**
+ * 是否自动统计数据,在最后追加一行统计数据总和
+ */
+ public boolean isStatistics() default false;
+
+ /**
+ * 导出类型(0数字 1字符串 2图片)
+ */
+ public ColumnType cellType() default ColumnType.STRING;
+
+ /**
+ * 导出列头背景颜色
+ */
+ public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT;
+
+ /**
+ * 导出列头字体颜色
+ */
+ public IndexedColors headerColor() default IndexedColors.WHITE;
+
+ /**
+ * 导出单元格背景颜色
+ */
+ public IndexedColors backgroundColor() default IndexedColors.WHITE;
+
+ /**
+ * 导出单元格字体颜色
+ */
+ public IndexedColors color() default IndexedColors.BLACK;
+
+ /**
+ * 导出字段对齐方式
+ */
+ public HorizontalAlignment align() default HorizontalAlignment.CENTER;
+
+ /**
+ * 自定义数据处理器
+ */
+ public Class<?> handler() default ExcelHandlerAdapter.class;
+
+ /**
+ * 自定义数据处理器参数
+ */
+ public String[] args() default {};
+
+ /**
+ * 字段类型(0:导出导入;1:仅导出;2:仅导入)
+ */
+ Type type() default Type.ALL;
+
+ public enum Type
+ {
+ ALL(0), EXPORT(1), IMPORT(2);
+ private final int value;
+
+ Type(int value)
+ {
+ this.value = value;
+ }
+
+ public int value()
+ {
+ return this.value;
+ }
+ }
+
+ public enum ColumnType
+ {
+ NUMERIC(0), STRING(1), IMAGE(2), TEXT(3);
+ private final int value;
+
+ ColumnType(int value)
+ {
+ this.value = value;
+ }
+
+ public int value()
+ {
+ return this.value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java
new file mode 100644
index 0000000..1f1cc81
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java
@@ -0,0 +1,18 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Excel注解集
+ *
+ * @author ruoyi
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Excels
+{
+ public Excel[] value();
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java
new file mode 100644
index 0000000..1eb8e49
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java
@@ -0,0 +1,51 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.enums.OperatorType;
+
+/**
+ * 自定义操作日志记录注解
+ *
+ * @author ruoyi
+ *
+ */
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log
+{
+ /**
+ * 模块
+ */
+ public String title() default "";
+
+ /**
+ * 功能
+ */
+ public BusinessType businessType() default BusinessType.OTHER;
+
+ /**
+ * 操作人类别
+ */
+ public OperatorType operatorType() default OperatorType.MANAGE;
+
+ /**
+ * 是否保存请求的参数
+ */
+ public boolean isSaveRequestData() default true;
+
+ /**
+ * 是否保存响应的参数
+ */
+ public boolean isSaveResponseData() default true;
+
+ /**
+ * 排除指定的请求参数
+ */
+ public String[] excludeParamNames() default {};
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java
new file mode 100644
index 0000000..0f024c7
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java
@@ -0,0 +1,40 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.enums.LimitType;
+
+/**
+ * 限流注解
+ *
+ * @author ruoyi
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter
+{
+ /**
+ * 限流key
+ */
+ public String key() default CacheConstants.RATE_LIMIT_KEY;
+
+ /**
+ * 限流时间,单位秒
+ */
+ public int time() default 60;
+
+ /**
+ * 限流次数
+ */
+ public int count() default 100;
+
+ /**
+ * 限流类型
+ */
+ public LimitType limitType() default LimitType.DEFAULT;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java
new file mode 100644
index 0000000..b769748
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java
@@ -0,0 +1,31 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义注解防止表单重复提交
+ *
+ * @author ruoyi
+ *
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit
+{
+ /**
+ * 间隔时间(ms),小于此时间视为重复提交
+ */
+ public int interval() default 5000;
+
+ /**
+ * 提示消息
+ */
+ public String message() default "不允许重复提交,请稍候再试";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java
new file mode 100644
index 0000000..c0621e9
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.ruoyi.common.config.serializer.SensitiveJsonSerializer;
+import com.ruoyi.common.enums.DesensitizedType;
+
+/**
+ * 数据脱敏注解
+ *
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@JacksonAnnotationsInside
+@JsonSerialize(using = SensitiveJsonSerializer.class)
+public @interface Sensitive
+{
+ DesensitizedType desensitizedType();
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java
new file mode 100644
index 0000000..29281cf
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java
@@ -0,0 +1,122 @@
+package com.ruoyi.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ *
+ * @author ruoyi
+ */
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class RuoYiConfig
+{
+ /** 项目名称 */
+ private String name;
+
+ /** 版本 */
+ private String version;
+
+ /** 版权年份 */
+ private String copyrightYear;
+
+ /** 上传路径 */
+ private static String profile;
+
+ /** 获取地址开关 */
+ private static boolean addressEnabled;
+
+ /** 验证码类型 */
+ private static String captchaType;
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getVersion()
+ {
+ return version;
+ }
+
+ public void setVersion(String version)
+ {
+ this.version = version;
+ }
+
+ public String getCopyrightYear()
+ {
+ return copyrightYear;
+ }
+
+ public void setCopyrightYear(String copyrightYear)
+ {
+ this.copyrightYear = copyrightYear;
+ }
+
+ public static String getProfile()
+ {
+ return profile;
+ }
+
+ public void setProfile(String profile)
+ {
+ RuoYiConfig.profile = profile;
+ }
+
+ public static boolean isAddressEnabled()
+ {
+ return addressEnabled;
+ }
+
+ public void setAddressEnabled(boolean addressEnabled)
+ {
+ RuoYiConfig.addressEnabled = addressEnabled;
+ }
+
+ public static String getCaptchaType() {
+ return captchaType;
+ }
+
+ public void setCaptchaType(String captchaType) {
+ RuoYiConfig.captchaType = captchaType;
+ }
+
+ /**
+ * 获取导入上传路径
+ */
+ public static String getImportPath()
+ {
+ return getProfile() + "/import";
+ }
+
+ /**
+ * 获取头像上传路径
+ */
+ public static String getAvatarPath()
+ {
+ return getProfile() + "/avatar";
+ }
+
+ /**
+ * 获取下载路径
+ */
+ public static String getDownloadPath()
+ {
+ return getProfile() + "/download/";
+ }
+
+ /**
+ * 获取上传路径
+ */
+ public static String getUploadPath()
+ {
+ return getProfile() + "/upload";
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java
new file mode 100644
index 0000000..e819a1d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java
@@ -0,0 +1,67 @@
+package com.ruoyi.common.config.serializer;
+
+import java.io.IOException;
+import java.util.Objects;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import com.ruoyi.common.annotation.Sensitive;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.DesensitizedType;
+import com.ruoyi.common.utils.SecurityUtils;
+
+/**
+ * 数据脱敏序列化过滤
+ *
+ * @author ruoyi
+ */
+public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer
+{
+ private DesensitizedType desensitizedType;
+
+ @Override
+ public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException
+ {
+ if (desensitization())
+ {
+ gen.writeString(desensitizedType.desensitizer().apply(value));
+ }
+ else
+ {
+ gen.writeString(value);
+ }
+ }
+
+ @Override
+ public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
+ throws JsonMappingException
+ {
+ Sensitive annotation = property.getAnnotation(Sensitive.class);
+ if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass()))
+ {
+ this.desensitizedType = annotation.desensitizedType();
+ return this;
+ }
+ return prov.findValueSerializer(property.getType(), property);
+ }
+
+ /**
+ * 是否需要脱敏处理
+ */
+ private boolean desensitization()
+ {
+ try
+ {
+ LoginUser securityUser = SecurityUtils.getLoginUser();
+ // 管理员不脱敏
+ return !securityUser.getUser().isAdmin();
+ }
+ catch (Exception e)
+ {
+ return true;
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
new file mode 100644
index 0000000..0080343
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
@@ -0,0 +1,44 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 缓存的key 常量
+ *
+ * @author ruoyi
+ */
+public class CacheConstants
+{
+ /**
+ * 登录用户 redis key
+ */
+ public static final String LOGIN_TOKEN_KEY = "login_tokens:";
+
+ /**
+ * 验证码 redis key
+ */
+ public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
+
+ /**
+ * 参数管理 cache key
+ */
+ public static final String SYS_CONFIG_KEY = "sys_config:";
+
+ /**
+ * 字典管理 cache key
+ */
+ public static final String SYS_DICT_KEY = "sys_dict:";
+
+ /**
+ * 防重提交 redis key
+ */
+ public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
+
+ /**
+ * 限流 redis key
+ */
+ public static final String RATE_LIMIT_KEY = "rate_limit:";
+
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java
new file mode 100644
index 0000000..0c384c6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java
@@ -0,0 +1,173 @@
+package com.ruoyi.common.constant;
+
+import java.util.Locale;
+import io.jsonwebtoken.Claims;
+
+/**
+ * 通用常量信息
+ *
+ * @author ruoyi
+ */
+public class Constants
+{
+ /**
+ * UTF-8 字符集
+ */
+ public static final String UTF8 = "UTF-8";
+
+ /**
+ * GBK 字符集
+ */
+ public static final String GBK = "GBK";
+
+ /**
+ * 系统语言
+ */
+ public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
+
+ /**
+ * www主域
+ */
+ public static final String WWW = "www.";
+
+ /**
+ * http请求
+ */
+ public static final String HTTP = "http://";
+
+ /**
+ * https请求
+ */
+ public static final String HTTPS = "https://";
+
+ /**
+ * 通用成功标识
+ */
+ public static final String SUCCESS = "0";
+
+ /**
+ * 通用失败标识
+ */
+ public static final String FAIL = "1";
+
+ /**
+ * 登录成功
+ */
+ public static final String LOGIN_SUCCESS = "Success";
+
+ /**
+ * 注销
+ */
+ public static final String LOGOUT = "Logout";
+
+ /**
+ * 注册
+ */
+ public static final String REGISTER = "Register";
+
+ /**
+ * 登录失败
+ */
+ public static final String LOGIN_FAIL = "Error";
+
+ /**
+ * 所有权限标识
+ */
+ public static final String ALL_PERMISSION = "*:*:*";
+
+ /**
+ * 管理员角色权限标识
+ */
+ public static final String SUPER_ADMIN = "admin";
+
+ /**
+ * 角色权限分隔符
+ */
+ public static final String ROLE_DELIMETER = ",";
+
+ /**
+ * 权限标识分隔符
+ */
+ public static final String PERMISSION_DELIMETER = ",";
+
+ /**
+ * 验证码有效期(分钟)
+ */
+ public static final Integer CAPTCHA_EXPIRATION = 2;
+
+ /**
+ * 令牌
+ */
+ public static final String TOKEN = "token";
+
+ /**
+ * 令牌前缀
+ */
+ public static final String TOKEN_PREFIX = "Bearer ";
+
+ /**
+ * 令牌前缀
+ */
+ public static final String LOGIN_USER_KEY = "login_user_key";
+
+ /**
+ * 用户ID
+ */
+ public static final String JWT_USERID = "userid";
+
+ /**
+ * 用户名称
+ */
+ public static final String JWT_USERNAME = Claims.SUBJECT;
+
+ /**
+ * 用户头像
+ */
+ public static final String JWT_AVATAR = "avatar";
+
+ /**
+ * 创建时间
+ */
+ public static final String JWT_CREATED = "created";
+
+ /**
+ * 用户权限
+ */
+ public static final String JWT_AUTHORITIES = "authorities";
+
+ /**
+ * 资源映射路径 前缀
+ */
+ public static final String RESOURCE_PREFIX = "/profile";
+
+ /**
+ * RMI 远程方法调用
+ */
+ public static final String LOOKUP_RMI = "rmi:";
+
+ /**
+ * LDAP 远程方法调用
+ */
+ public static final String LOOKUP_LDAP = "ldap:";
+
+ /**
+ * LDAPS 远程方法调用
+ */
+ public static final String LOOKUP_LDAPS = "ldaps:";
+
+ /**
+ * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
+ */
+ public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" };
+
+ /**
+ * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+ */
+ public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" };
+
+ /**
+ * 定时任务违规的字符
+ */
+ public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
+ "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" };
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java
new file mode 100644
index 0000000..7d899d4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java
@@ -0,0 +1,117 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 代码生成通用常量
+ *
+ * @author ruoyi
+ */
+public class GenConstants
+{
+ /** 单表(增删改查) */
+ public static final String TPL_CRUD = "crud";
+
+ /** 树表(增删改查) */
+ public static final String TPL_TREE = "tree";
+
+ /** 主子表(增删改查) */
+ public static final String TPL_SUB = "sub";
+
+ /** 树编码字段 */
+ public static final String TREE_CODE = "treeCode";
+
+ /** 树父编码字段 */
+ public static final String TREE_PARENT_CODE = "treeParentCode";
+
+ /** 树名称字段 */
+ public static final String TREE_NAME = "treeName";
+
+ /** 上级菜单ID字段 */
+ public static final String PARENT_MENU_ID = "parentMenuId";
+
+ /** 上级菜单名称字段 */
+ public static final String PARENT_MENU_NAME = "parentMenuName";
+
+ /** 数据库字符串类型 */
+ public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" };
+
+ /** 数据库文本类型 */
+ public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" };
+
+ /** 数据库时间类型 */
+ public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" };
+
+ /** 数据库数字类型 */
+ public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer",
+ "bit", "bigint", "float", "double", "decimal" };
+
+ /** 页面不需要编辑字段 */
+ public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" };
+
+ /** 页面不需要显示的列表字段 */
+ public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by",
+ "update_time" };
+
+ /** 页面不需要查询字段 */
+ public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by",
+ "update_time", "remark" };
+
+ /** Entity基类字段 */
+ public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" };
+
+ /** Tree基类字段 */
+ public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" };
+
+ /** 文本框 */
+ public static final String HTML_INPUT = "input";
+
+ /** 文本域 */
+ public static final String HTML_TEXTAREA = "textarea";
+
+ /** 下拉框 */
+ public static final String HTML_SELECT = "select";
+
+ /** 单选框 */
+ public static final String HTML_RADIO = "radio";
+
+ /** 复选框 */
+ public static final String HTML_CHECKBOX = "checkbox";
+
+ /** 日期控件 */
+ public static final String HTML_DATETIME = "datetime";
+
+ /** 图片上传控件 */
+ public static final String HTML_IMAGE_UPLOAD = "imageUpload";
+
+ /** 文件上传控件 */
+ public static final String HTML_FILE_UPLOAD = "fileUpload";
+
+ /** 富文本控件 */
+ public static final String HTML_EDITOR = "editor";
+
+ /** 字符串类型 */
+ public static final String TYPE_STRING = "String";
+
+ /** 整型 */
+ public static final String TYPE_INTEGER = "Integer";
+
+ /** 长整型 */
+ public static final String TYPE_LONG = "Long";
+
+ /** 浮点型 */
+ public static final String TYPE_DOUBLE = "Double";
+
+ /** 高精度计算类型 */
+ public static final String TYPE_BIGDECIMAL = "BigDecimal";
+
+ /** 时间类型 */
+ public static final String TYPE_DATE = "Date";
+
+ /** 模糊查询 */
+ public static final String QUERY_LIKE = "LIKE";
+
+ /** 相等查询 */
+ public static final String QUERY_EQ = "EQ";
+
+ /** 需要 */
+ public static final String REQUIRE = "1";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java
new file mode 100644
index 0000000..a983c77
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java
@@ -0,0 +1,94 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 返回状态码
+ *
+ * @author ruoyi
+ */
+public class HttpStatus
+{
+ /**
+ * 操作成功
+ */
+ public static final int SUCCESS = 200;
+
+ /**
+ * 对象创建成功
+ */
+ public static final int CREATED = 201;
+
+ /**
+ * 请求已经被接受
+ */
+ public static final int ACCEPTED = 202;
+
+ /**
+ * 操作已经执行成功,但是没有返回数据
+ */
+ public static final int NO_CONTENT = 204;
+
+ /**
+ * 资源已被移除
+ */
+ public static final int MOVED_PERM = 301;
+
+ /**
+ * 重定向
+ */
+ public static final int SEE_OTHER = 303;
+
+ /**
+ * 资源没有被修改
+ */
+ public static final int NOT_MODIFIED = 304;
+
+ /**
+ * 参数列表错误(缺少,格式不匹配)
+ */
+ public static final int BAD_REQUEST = 400;
+
+ /**
+ * 未授权
+ */
+ public static final int UNAUTHORIZED = 401;
+
+ /**
+ * 访问受限,授权过期
+ */
+ public static final int FORBIDDEN = 403;
+
+ /**
+ * 资源,服务未找到
+ */
+ public static final int NOT_FOUND = 404;
+
+ /**
+ * 不允许的http方法
+ */
+ public static final int BAD_METHOD = 405;
+
+ /**
+ * 资源冲突,或者资源被锁
+ */
+ public static final int CONFLICT = 409;
+
+ /**
+ * 不支持的数据,媒体类型
+ */
+ public static final int UNSUPPORTED_TYPE = 415;
+
+ /**
+ * 系统内部错误
+ */
+ public static final int ERROR = 500;
+
+ /**
+ * 接口未实现
+ */
+ public static final int NOT_IMPLEMENTED = 501;
+
+ /**
+ * 系统警告消息
+ */
+ public static final int WARN = 601;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java
new file mode 100644
index 0000000..62ad815
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java
@@ -0,0 +1,50 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 任务调度通用常量
+ *
+ * @author ruoyi
+ */
+public class ScheduleConstants
+{
+ public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME";
+
+ /** 执行目标key */
+ public static final String TASK_PROPERTIES = "TASK_PROPERTIES";
+
+ /** 默认 */
+ public static final String MISFIRE_DEFAULT = "0";
+
+ /** 立即触发执行 */
+ public static final String MISFIRE_IGNORE_MISFIRES = "1";
+
+ /** 触发一次执行 */
+ public static final String MISFIRE_FIRE_AND_PROCEED = "2";
+
+ /** 不触发立即执行 */
+ public static final String MISFIRE_DO_NOTHING = "3";
+
+ public enum Status
+ {
+ /**
+ * 正常
+ */
+ NORMAL("0"),
+ /**
+ * 暂停
+ */
+ PAUSE("1");
+
+ private String value;
+
+ private Status(String value)
+ {
+ this.value = value;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
new file mode 100644
index 0000000..8dc7faa
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
@@ -0,0 +1,81 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public class UserConstants
+{
+ /**
+ * 平台内系统用户的唯一标志
+ */
+ public static final String SYS_USER = "SYS_USER";
+
+ /** 正常状态 */
+ public static final String NORMAL = "0";
+
+ /** 异常状态 */
+ public static final String EXCEPTION = "1";
+
+ /** 用户封禁状态 */
+ public static final String USER_DISABLE = "1";
+
+ /** 角色正常状态 */
+ public static final String ROLE_NORMAL = "0";
+
+ /** 角色封禁状态 */
+ public static final String ROLE_DISABLE = "1";
+
+ /** 部门正常状态 */
+ public static final String DEPT_NORMAL = "0";
+
+ /** 部门停用状态 */
+ public static final String DEPT_DISABLE = "1";
+
+ /** 字典正常状态 */
+ public static final String DICT_NORMAL = "0";
+
+ /** 是否为系统默认(是) */
+ public static final String YES = "Y";
+
+ /** 是否菜单外链(是) */
+ public static final String YES_FRAME = "0";
+
+ /** 是否菜单外链(否) */
+ public static final String NO_FRAME = "1";
+
+ /** 菜单类型(目录) */
+ public static final String TYPE_DIR = "M";
+
+ /** 菜单类型(菜单) */
+ public static final String TYPE_MENU = "C";
+
+ /** 菜单类型(按钮) */
+ public static final String TYPE_BUTTON = "F";
+
+ /** Layout组件标识 */
+ public final static String LAYOUT = "Layout";
+
+ /** ParentView组件标识 */
+ public final static String PARENT_VIEW = "ParentView";
+
+ /** InnerLink组件标识 */
+ public final static String INNER_LINK = "InnerLink";
+
+ /** 校验是否唯一的返回标识 */
+ public final static boolean UNIQUE = true;
+ public final static boolean NOT_UNIQUE = false;
+
+ /**
+ * 用户名长度限制
+ */
+ public static final int USERNAME_MIN_LENGTH = 2;
+ public static final int USERNAME_MAX_LENGTH = 20;
+
+ /**
+ * 密码长度限制
+ */
+ public static final int PASSWORD_MIN_LENGTH = 5;
+ public static final int PASSWORD_MAX_LENGTH = 20;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java
new file mode 100644
index 0000000..a685e06
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java
@@ -0,0 +1,202 @@
+package com.ruoyi.common.core.controller;
+
+import java.beans.PropertyEditorSupport;
+import java.util.Date;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.InitBinder;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.page.PageDomain;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.page.TableSupport;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.PageUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.sql.SqlUtil;
+
+/**
+ * web层通用数据处理
+ *
+ * @author ruoyi
+ */
+public class BaseController
+{
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /**
+ * 将前台传递过来的日期格式的字符串,自动转化为Date类型
+ */
+ @InitBinder
+ public void initBinder(WebDataBinder binder)
+ {
+ // Date 类型转换
+ binder.registerCustomEditor(Date.class, new PropertyEditorSupport()
+ {
+ @Override
+ public void setAsText(String text)
+ {
+ setValue(DateUtils.parseDate(text));
+ }
+ });
+ }
+
+ /**
+ * 设置请求分页数据
+ */
+ protected void startPage()
+ {
+ PageUtils.startPage();
+ }
+
+ /**
+ * 设置请求排序数据
+ */
+ protected void startOrderBy()
+ {
+ PageDomain pageDomain = TableSupport.buildPageRequest();
+ if (StringUtils.isNotEmpty(pageDomain.getOrderBy()))
+ {
+ String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+ PageHelper.orderBy(orderBy);
+ }
+ }
+
+ /**
+ * 清理分页的线程变量
+ */
+ protected void clearPage()
+ {
+ PageUtils.clearPage();
+ }
+
+ /**
+ * 响应请求分页数据
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ protected TableDataInfo getDataTable(List<?> list)
+ {
+ TableDataInfo rspData = new TableDataInfo();
+ rspData.setCode(HttpStatus.SUCCESS);
+ rspData.setMsg("查询成功");
+ rspData.setRows(list);
+ rspData.setTotal(new PageInfo(list).getTotal());
+ return rspData;
+ }
+
+ /**
+ * 返回成功
+ */
+ public AjaxResult success()
+ {
+ return AjaxResult.success();
+ }
+
+ /**
+ * 返回失败消息
+ */
+ public AjaxResult error()
+ {
+ return AjaxResult.error();
+ }
+
+ /**
+ * 返回成功消息
+ */
+ public AjaxResult success(String message)
+ {
+ return AjaxResult.success(message);
+ }
+
+ /**
+ * 返回成功消息
+ */
+ public AjaxResult success(Object data)
+ {
+ return AjaxResult.success(data);
+ }
+
+ /**
+ * 返回失败消息
+ */
+ public AjaxResult error(String message)
+ {
+ return AjaxResult.error(message);
+ }
+
+ /**
+ * 返回警告消息
+ */
+ public AjaxResult warn(String message)
+ {
+ return AjaxResult.warn(message);
+ }
+
+ /**
+ * 响应返回结果
+ *
+ * @param rows 影响行数
+ * @return 操作结果
+ */
+ protected AjaxResult toAjax(int rows)
+ {
+ return rows > 0 ? AjaxResult.success() : AjaxResult.error();
+ }
+
+ /**
+ * 响应返回结果
+ *
+ * @param result 结果
+ * @return 操作结果
+ */
+ protected AjaxResult toAjax(boolean result)
+ {
+ return result ? success() : error();
+ }
+
+ /**
+ * 页面跳转
+ */
+ public String redirect(String url)
+ {
+ return StringUtils.format("redirect:{}", url);
+ }
+
+ /**
+ * 获取用户缓存信息
+ */
+ public LoginUser getLoginUser()
+ {
+ return SecurityUtils.getLoginUser();
+ }
+
+ /**
+ * 获取登录用户id
+ */
+ public Long getUserId()
+ {
+ return getLoginUser().getUserId();
+ }
+
+ /**
+ * 获取登录部门id
+ */
+ public Long getDeptId()
+ {
+ return getLoginUser().getDeptId();
+ }
+
+ /**
+ * 获取登录用户名
+ */
+ public String getUsername()
+ {
+ return getLoginUser().getUsername();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java
new file mode 100644
index 0000000..a7abfe4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java
@@ -0,0 +1,216 @@
+package com.ruoyi.common.core.domain;
+
+import java.util.HashMap;
+import java.util.Objects;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 操作消息提醒
+ *
+ * @author ruoyi
+ */
+public class AjaxResult extends HashMap<String, Object>
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 状态码 */
+ public static final String CODE_TAG = "code";
+
+ /** 返回内容 */
+ public static final String MSG_TAG = "msg";
+
+ /** 数据对象 */
+ public static final String DATA_TAG = "data";
+
+ /**
+ * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
+ */
+ public AjaxResult()
+ {
+ }
+
+ /**
+ * 初始化一个新创建的 AjaxResult 对象
+ *
+ * @param code 状态码
+ * @param msg 返回内容
+ */
+ public AjaxResult(int code, String msg)
+ {
+ super.put(CODE_TAG, code);
+ super.put(MSG_TAG, msg);
+ }
+
+ /**
+ * 初始化一个新创建的 AjaxResult 对象
+ *
+ * @param code 状态码
+ * @param msg 返回内容
+ * @param data 数据对象
+ */
+ public AjaxResult(int code, String msg, Object data)
+ {
+ super.put(CODE_TAG, code);
+ super.put(MSG_TAG, msg);
+ if (StringUtils.isNotNull(data))
+ {
+ super.put(DATA_TAG, data);
+ }
+ }
+
+ /**
+ * 返回成功消息
+ *
+ * @return 成功消息
+ */
+ public static AjaxResult success()
+ {
+ return AjaxResult.success("操作成功");
+ }
+
+ /**
+ * 返回成功数据
+ *
+ * @return 成功消息
+ */
+ public static AjaxResult success(Object data)
+ {
+ return AjaxResult.success("操作成功", data);
+ }
+
+ /**
+ * 返回成功消息
+ *
+ * @param msg 返回内容
+ * @return 成功消息
+ */
+ public static AjaxResult success(String msg)
+ {
+ return AjaxResult.success(msg, null);
+ }
+
+ /**
+ * 返回成功消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 成功消息
+ */
+ public static AjaxResult success(String msg, Object data)
+ {
+ return new AjaxResult(HttpStatus.SUCCESS, msg, data);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @return 警告消息
+ */
+ public static AjaxResult warn(String msg)
+ {
+ return AjaxResult.warn(msg, null);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 警告消息
+ */
+ public static AjaxResult warn(String msg, Object data)
+ {
+ return new AjaxResult(HttpStatus.WARN, msg, data);
+ }
+
+ /**
+ * 返回错误消息
+ *
+ * @return 错误消息
+ */
+ public static AjaxResult error()
+ {
+ return AjaxResult.error("操作失败");
+ }
+
+ /**
+ * 返回错误消息
+ *
+ * @param msg 返回内容
+ * @return 错误消息
+ */
+ public static AjaxResult error(String msg)
+ {
+ return AjaxResult.error(msg, null);
+ }
+
+ /**
+ * 返回错误消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 错误消息
+ */
+ public static AjaxResult error(String msg, Object data)
+ {
+ return new AjaxResult(HttpStatus.ERROR, msg, data);
+ }
+
+ /**
+ * 返回错误消息
+ *
+ * @param code 状态码
+ * @param msg 返回内容
+ * @return 错误消息
+ */
+ public static AjaxResult error(int code, String msg)
+ {
+ return new AjaxResult(code, msg, null);
+ }
+
+ /**
+ * 是否为成功消息
+ *
+ * @return 结果
+ */
+ public boolean isSuccess()
+ {
+ return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG));
+ }
+
+ /**
+ * 是否为警告消息
+ *
+ * @return 结果
+ */
+ public boolean isWarn()
+ {
+ return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG));
+ }
+
+ /**
+ * 是否为错误消息
+ *
+ * @return 结果
+ */
+ public boolean isError()
+ {
+ return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));
+ }
+
+ /**
+ * 方便链式调用
+ *
+ * @param key 键
+ * @param value 值
+ * @return 数据对象
+ */
+ @Override
+ public AjaxResult put(String key, Object value)
+ {
+ super.put(key, value);
+ return this;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
new file mode 100644
index 0000000..15bf66b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
@@ -0,0 +1,118 @@
+package com.ruoyi.common.core.domain;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * Entity基类
+ *
+ * @author ruoyi
+ */
+public class BaseEntity implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 搜索值 */
+ @JsonIgnore
+ private String searchValue;
+
+ /** 创建者 */
+ private String createBy;
+
+ /** 创建时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /** 更新者 */
+ private String updateBy;
+
+ /** 更新时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date updateTime;
+
+ /** 备注 */
+ private String remark;
+
+ /** 请求参数 */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private Map<String, Object> params;
+
+ public String getSearchValue()
+ {
+ return searchValue;
+ }
+
+ public void setSearchValue(String searchValue)
+ {
+ this.searchValue = searchValue;
+ }
+
+ public String getCreateBy()
+ {
+ return createBy;
+ }
+
+ public void setCreateBy(String createBy)
+ {
+ this.createBy = createBy;
+ }
+
+ public Date getCreateTime()
+ {
+ return createTime;
+ }
+
+ public void setCreateTime(Date createTime)
+ {
+ this.createTime = createTime;
+ }
+
+ public String getUpdateBy()
+ {
+ return updateBy;
+ }
+
+ public void setUpdateBy(String updateBy)
+ {
+ this.updateBy = updateBy;
+ }
+
+ public Date getUpdateTime()
+ {
+ return updateTime;
+ }
+
+ public void setUpdateTime(Date updateTime)
+ {
+ this.updateTime = updateTime;
+ }
+
+ public String getRemark()
+ {
+ return remark;
+ }
+
+ public void setRemark(String remark)
+ {
+ this.remark = remark;
+ }
+
+ public Map<String, Object> getParams()
+ {
+ if (params == null)
+ {
+ params = new HashMap<>();
+ }
+ return params;
+ }
+
+ public void setParams(Map<String, Object> params)
+ {
+ this.params = params;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java
new file mode 100644
index 0000000..ef15802
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java
@@ -0,0 +1,115 @@
+package com.ruoyi.common.core.domain;
+
+import java.io.Serializable;
+import com.ruoyi.common.constant.HttpStatus;
+
+/**
+ * 响应信息主体
+ *
+ * @author ruoyi
+ */
+public class R<T> implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 成功 */
+ public static final int SUCCESS = HttpStatus.SUCCESS;
+
+ /** 失败 */
+ public static final int FAIL = HttpStatus.ERROR;
+
+ private int code;
+
+ private String msg;
+
+ private T data;
+
+ public static <T> R<T> ok()
+ {
+ return restResult(null, SUCCESS, "操作成功");
+ }
+
+ public static <T> R<T> ok(T data)
+ {
+ return restResult(data, SUCCESS, "操作成功");
+ }
+
+ public static <T> R<T> ok(T data, String msg)
+ {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static <T> R<T> fail()
+ {
+ return restResult(null, FAIL, "操作失败");
+ }
+
+ public static <T> R<T> fail(String msg)
+ {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static <T> R<T> fail(T data)
+ {
+ return restResult(data, FAIL, "操作失败");
+ }
+
+ public static <T> R<T> fail(T data, String msg)
+ {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static <T> R<T> fail(int code, String msg)
+ {
+ return restResult(null, code, msg);
+ }
+
+ private static <T> R<T> restResult(T data, int code, String msg)
+ {
+ R<T> apiResult = new R<>();
+ apiResult.setCode(code);
+ apiResult.setData(data);
+ apiResult.setMsg(msg);
+ return apiResult;
+ }
+
+ public int getCode()
+ {
+ return code;
+ }
+
+ public void setCode(int code)
+ {
+ this.code = code;
+ }
+
+ public String getMsg()
+ {
+ return msg;
+ }
+
+ public void setMsg(String msg)
+ {
+ this.msg = msg;
+ }
+
+ public T getData()
+ {
+ return data;
+ }
+
+ public void setData(T data)
+ {
+ this.data = data;
+ }
+
+ public static <T> Boolean isError(R<T> ret)
+ {
+ return !isSuccess(ret);
+ }
+
+ public static <T> Boolean isSuccess(R<T> ret)
+ {
+ return R.SUCCESS == ret.getCode();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java
new file mode 100644
index 0000000..a180a18
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java
@@ -0,0 +1,79 @@
+package com.ruoyi.common.core.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tree基类
+ *
+ * @author ruoyi
+ */
+public class TreeEntity extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 父菜单名称 */
+ private String parentName;
+
+ /** 父菜单ID */
+ private Long parentId;
+
+ /** 显示顺序 */
+ private Integer orderNum;
+
+ /** 祖级列表 */
+ private String ancestors;
+
+ /** 子部门 */
+ private List<?> children = new ArrayList<>();
+
+ public String getParentName()
+ {
+ return parentName;
+ }
+
+ public void setParentName(String parentName)
+ {
+ this.parentName = parentName;
+ }
+
+ public Long getParentId()
+ {
+ return parentId;
+ }
+
+ public void setParentId(Long parentId)
+ {
+ this.parentId = parentId;
+ }
+
+ public Integer getOrderNum()
+ {
+ return orderNum;
+ }
+
+ public void setOrderNum(Integer orderNum)
+ {
+ this.orderNum = orderNum;
+ }
+
+ public String getAncestors()
+ {
+ return ancestors;
+ }
+
+ public void setAncestors(String ancestors)
+ {
+ this.ancestors = ancestors;
+ }
+
+ public List<?> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<?> children)
+ {
+ this.children = children;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java
new file mode 100644
index 0000000..bd835db
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java
@@ -0,0 +1,77 @@
+package com.ruoyi.common.core.domain;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+
+/**
+ * Treeselect树结构实体类
+ *
+ * @author ruoyi
+ */
+public class TreeSelect implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 节点ID */
+ private Long id;
+
+ /** 节点名称 */
+ private String label;
+
+ /** 子节点 */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private List<TreeSelect> children;
+
+ public TreeSelect()
+ {
+
+ }
+
+ public TreeSelect(SysDept dept)
+ {
+ this.id = dept.getDeptId();
+ this.label = dept.getDeptName();
+ this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
+ }
+
+ public TreeSelect(SysMenu menu)
+ {
+ this.id = menu.getMenuId();
+ this.label = menu.getMenuName();
+ this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
+ }
+
+ public Long getId()
+ {
+ return id;
+ }
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public String getLabel()
+ {
+ return label;
+ }
+
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+ public List<TreeSelect> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<TreeSelect> children)
+ {
+ this.children = children;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java
new file mode 100644
index 0000000..37236db
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java
@@ -0,0 +1,203 @@
+package com.ruoyi.common.core.domain.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 部门表 sys_dept
+ *
+ * @author ruoyi
+ */
+public class SysDept extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 部门ID */
+ private Long deptId;
+
+ /** 父部门ID */
+ private Long parentId;
+
+ /** 祖级列表 */
+ private String ancestors;
+
+ /** 部门名称 */
+ private String deptName;
+
+ /** 显示顺序 */
+ private Integer orderNum;
+
+ /** 负责人 */
+ private String leader;
+
+ /** 联系电话 */
+ private String phone;
+
+ /** 邮箱 */
+ private String email;
+
+ /** 部门状态:0正常,1停用 */
+ private String status;
+
+ /** 删除标志(0代表存在 2代表删除) */
+ private String delFlag;
+
+ /** 父部门名称 */
+ private String parentName;
+
+ /** 子部门 */
+ private List<SysDept> children = new ArrayList<SysDept>();
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ public Long getParentId()
+ {
+ return parentId;
+ }
+
+ public void setParentId(Long parentId)
+ {
+ this.parentId = parentId;
+ }
+
+ public String getAncestors()
+ {
+ return ancestors;
+ }
+
+ public void setAncestors(String ancestors)
+ {
+ this.ancestors = ancestors;
+ }
+
+ @NotBlank(message = "部门名称不能为空")
+ @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符")
+ public String getDeptName()
+ {
+ return deptName;
+ }
+
+ public void setDeptName(String deptName)
+ {
+ this.deptName = deptName;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getOrderNum()
+ {
+ return orderNum;
+ }
+
+ public void setOrderNum(Integer orderNum)
+ {
+ this.orderNum = orderNum;
+ }
+
+ public String getLeader()
+ {
+ return leader;
+ }
+
+ public void setLeader(String leader)
+ {
+ this.leader = leader;
+ }
+
+ @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符")
+ public String getPhone()
+ {
+ return phone;
+ }
+
+ public void setPhone(String phone)
+ {
+ this.phone = phone;
+ }
+
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
+ public String getEmail()
+ {
+ return email;
+ }
+
+ public void setEmail(String email)
+ {
+ this.email = email;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ public String getParentName()
+ {
+ return parentName;
+ }
+
+ public void setParentName(String parentName)
+ {
+ this.parentName = parentName;
+ }
+
+ public List<SysDept> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<SysDept> children)
+ {
+ this.children = children;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("deptId", getDeptId())
+ .append("parentId", getParentId())
+ .append("ancestors", getAncestors())
+ .append("deptName", getDeptName())
+ .append("orderNum", getOrderNum())
+ .append("leader", getLeader())
+ .append("phone", getPhone())
+ .append("email", getEmail())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java
new file mode 100644
index 0000000..6b035a6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java
@@ -0,0 +1,176 @@
+package com.ruoyi.common.core.domain.entity;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 字典数据表 sys_dict_data
+ *
+ * @author ruoyi
+ */
+public class SysDictData extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 字典编码 */
+ @Excel(name = "字典编码", cellType = ColumnType.NUMERIC)
+ private Long dictCode;
+
+ /** 字典排序 */
+ @Excel(name = "字典排序", cellType = ColumnType.NUMERIC)
+ private Long dictSort;
+
+ /** 字典标签 */
+ @Excel(name = "字典标签")
+ private String dictLabel;
+
+ /** 字典键值 */
+ @Excel(name = "字典键值")
+ private String dictValue;
+
+ /** 字典类型 */
+ @Excel(name = "字典类型")
+ private String dictType;
+
+ /** 样式属性(其他样式扩展) */
+ private String cssClass;
+
+ /** 表格字典样式 */
+ private String listClass;
+
+ /** 是否默认(Y是 N否) */
+ @Excel(name = "是否默认", readConverterExp = "Y=是,N=否")
+ private String isDefault;
+
+ /** 状态(0正常 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ public Long getDictCode()
+ {
+ return dictCode;
+ }
+
+ public void setDictCode(Long dictCode)
+ {
+ this.dictCode = dictCode;
+ }
+
+ public Long getDictSort()
+ {
+ return dictSort;
+ }
+
+ public void setDictSort(Long dictSort)
+ {
+ this.dictSort = dictSort;
+ }
+
+ @NotBlank(message = "字典标签不能为空")
+ @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符")
+ public String getDictLabel()
+ {
+ return dictLabel;
+ }
+
+ public void setDictLabel(String dictLabel)
+ {
+ this.dictLabel = dictLabel;
+ }
+
+ @NotBlank(message = "字典键值不能为空")
+ @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符")
+ public String getDictValue()
+ {
+ return dictValue;
+ }
+
+ public void setDictValue(String dictValue)
+ {
+ this.dictValue = dictValue;
+ }
+
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符")
+ public String getDictType()
+ {
+ return dictType;
+ }
+
+ public void setDictType(String dictType)
+ {
+ this.dictType = dictType;
+ }
+
+ @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符")
+ public String getCssClass()
+ {
+ return cssClass;
+ }
+
+ public void setCssClass(String cssClass)
+ {
+ this.cssClass = cssClass;
+ }
+
+ public String getListClass()
+ {
+ return listClass;
+ }
+
+ public void setListClass(String listClass)
+ {
+ this.listClass = listClass;
+ }
+
+ public boolean getDefault()
+ {
+ return UserConstants.YES.equals(this.isDefault);
+ }
+
+ public String getIsDefault()
+ {
+ return isDefault;
+ }
+
+ public void setIsDefault(String isDefault)
+ {
+ this.isDefault = isDefault;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("dictCode", getDictCode())
+ .append("dictSort", getDictSort())
+ .append("dictLabel", getDictLabel())
+ .append("dictValue", getDictValue())
+ .append("dictType", getDictType())
+ .append("cssClass", getCssClass())
+ .append("listClass", getListClass())
+ .append("isDefault", getIsDefault())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java
new file mode 100644
index 0000000..ac5d290
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java
@@ -0,0 +1,96 @@
+package com.ruoyi.common.core.domain.entity;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 字典类型表 sys_dict_type
+ *
+ * @author ruoyi
+ */
+public class SysDictType extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 字典主键 */
+ @Excel(name = "字典主键", cellType = ColumnType.NUMERIC)
+ private Long dictId;
+
+ /** 字典名称 */
+ @Excel(name = "字典名称")
+ private String dictName;
+
+ /** 字典类型 */
+ @Excel(name = "字典类型")
+ private String dictType;
+
+ /** 状态(0正常 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ public Long getDictId()
+ {
+ return dictId;
+ }
+
+ public void setDictId(Long dictId)
+ {
+ this.dictId = dictId;
+ }
+
+ @NotBlank(message = "字典名称不能为空")
+ @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符")
+ public String getDictName()
+ {
+ return dictName;
+ }
+
+ public void setDictName(String dictName)
+ {
+ this.dictName = dictName;
+ }
+
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符")
+ @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)")
+ public String getDictType()
+ {
+ return dictType;
+ }
+
+ public void setDictType(String dictType)
+ {
+ this.dictType = dictType;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("dictId", getDictId())
+ .append("dictName", getDictName())
+ .append("dictType", getDictType())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java
new file mode 100644
index 0000000..d4ab762
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java
@@ -0,0 +1,274 @@
+package com.ruoyi.common.core.domain.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 菜单权限表 sys_menu
+ *
+ * @author ruoyi
+ */
+public class SysMenu extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 菜单ID */
+ private Long menuId;
+
+ /** 菜单名称 */
+ private String menuName;
+
+ /** 父菜单名称 */
+ private String parentName;
+
+ /** 父菜单ID */
+ private Long parentId;
+
+ /** 显示顺序 */
+ private Integer orderNum;
+
+ /** 路由地址 */
+ private String path;
+
+ /** 组件路径 */
+ private String component;
+
+ /** 路由参数 */
+ private String query;
+
+ /** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */
+ private String routeName;
+
+ /** 是否为外链(0是 1否) */
+ private String isFrame;
+
+ /** 是否缓存(0缓存 1不缓存) */
+ private String isCache;
+
+ /** 类型(M目录 C菜单 F按钮) */
+ private String menuType;
+
+ /** 显示状态(0显示 1隐藏) */
+ private String visible;
+
+ /** 菜单状态(0正常 1停用) */
+ private String status;
+
+ /** 权限字符串 */
+ private String perms;
+
+ /** 菜单图标 */
+ private String icon;
+
+ /** 子菜单 */
+ private List<SysMenu> children = new ArrayList<SysMenu>();
+
+ public Long getMenuId()
+ {
+ return menuId;
+ }
+
+ public void setMenuId(Long menuId)
+ {
+ this.menuId = menuId;
+ }
+
+ @NotBlank(message = "菜单名称不能为空")
+ @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符")
+ public String getMenuName()
+ {
+ return menuName;
+ }
+
+ public void setMenuName(String menuName)
+ {
+ this.menuName = menuName;
+ }
+
+ public String getParentName()
+ {
+ return parentName;
+ }
+
+ public void setParentName(String parentName)
+ {
+ this.parentName = parentName;
+ }
+
+ public Long getParentId()
+ {
+ return parentId;
+ }
+
+ public void setParentId(Long parentId)
+ {
+ this.parentId = parentId;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getOrderNum()
+ {
+ return orderNum;
+ }
+
+ public void setOrderNum(Integer orderNum)
+ {
+ this.orderNum = orderNum;
+ }
+
+ @Size(min = 0, max = 200, message = "路由地址不能超过200个字符")
+ public String getPath()
+ {
+ return path;
+ }
+
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+ @Size(min = 0, max = 200, message = "组件路径不能超过255个字符")
+ public String getComponent()
+ {
+ return component;
+ }
+
+ public void setComponent(String component)
+ {
+ this.component = component;
+ }
+
+ public String getQuery()
+ {
+ return query;
+ }
+
+ public void setQuery(String query)
+ {
+ this.query = query;
+ }
+
+ public String getRouteName()
+ {
+ return routeName;
+ }
+
+ public void setRouteName(String routeName)
+ {
+ this.routeName = routeName;
+ }
+
+ public String getIsFrame()
+ {
+ return isFrame;
+ }
+
+ public void setIsFrame(String isFrame)
+ {
+ this.isFrame = isFrame;
+ }
+
+ public String getIsCache()
+ {
+ return isCache;
+ }
+
+ public void setIsCache(String isCache)
+ {
+ this.isCache = isCache;
+ }
+
+ @NotBlank(message = "菜单类型不能为空")
+ public String getMenuType()
+ {
+ return menuType;
+ }
+
+ public void setMenuType(String menuType)
+ {
+ this.menuType = menuType;
+ }
+
+ public String getVisible()
+ {
+ return visible;
+ }
+
+ public void setVisible(String visible)
+ {
+ this.visible = visible;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符")
+ public String getPerms()
+ {
+ return perms;
+ }
+
+ public void setPerms(String perms)
+ {
+ this.perms = perms;
+ }
+
+ public String getIcon()
+ {
+ return icon;
+ }
+
+ public void setIcon(String icon)
+ {
+ this.icon = icon;
+ }
+
+ public List<SysMenu> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<SysMenu> children)
+ {
+ this.children = children;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("menuId", getMenuId())
+ .append("menuName", getMenuName())
+ .append("parentId", getParentId())
+ .append("orderNum", getOrderNum())
+ .append("path", getPath())
+ .append("component", getComponent())
+ .append("query", getQuery())
+ .append("routeName", getRouteName())
+ .append("isFrame", getIsFrame())
+ .append("IsCache", getIsCache())
+ .append("menuType", getMenuType())
+ .append("visible", getVisible())
+ .append("status ", getStatus())
+ .append("perms", getPerms())
+ .append("icon", getIcon())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java
new file mode 100644
index 0000000..2f8ed14
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java
@@ -0,0 +1,241 @@
+package com.ruoyi.common.core.domain.entity;
+
+import java.util.Set;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 角色表 sys_role
+ *
+ * @author ruoyi
+ */
+public class SysRole extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 角色ID */
+ @Excel(name = "角色序号", cellType = ColumnType.NUMERIC)
+ private Long roleId;
+
+ /** 角色名称 */
+ @Excel(name = "角色名称")
+ private String roleName;
+
+ /** 角色权限 */
+ @Excel(name = "角色权限")
+ private String roleKey;
+
+ /** 角色排序 */
+ @Excel(name = "角色排序")
+ private Integer roleSort;
+
+ /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */
+ @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限")
+ private String dataScope;
+
+ /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
+ private boolean menuCheckStrictly;
+
+ /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
+ private boolean deptCheckStrictly;
+
+ /** 角色状态(0正常 1停用) */
+ @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /** 删除标志(0代表存在 2代表删除) */
+ private String delFlag;
+
+ /** 用户是否存在此角色标识 默认不存在 */
+ private boolean flag = false;
+
+ /** 菜单组 */
+ private Long[] menuIds;
+
+ /** 部门组(数据权限) */
+ private Long[] deptIds;
+
+ /** 角色菜单权限 */
+ private Set<String> permissions;
+
+ public SysRole()
+ {
+
+ }
+
+ public SysRole(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ public Long getRoleId()
+ {
+ return roleId;
+ }
+
+ public void setRoleId(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ public boolean isAdmin()
+ {
+ return isAdmin(this.roleId);
+ }
+
+ public static boolean isAdmin(Long roleId)
+ {
+ return roleId != null && 1L == roleId;
+ }
+
+ @NotBlank(message = "角色名称不能为空")
+ @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符")
+ public String getRoleName()
+ {
+ return roleName;
+ }
+
+ public void setRoleName(String roleName)
+ {
+ this.roleName = roleName;
+ }
+
+ @NotBlank(message = "权限字符不能为空")
+ @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符")
+ public String getRoleKey()
+ {
+ return roleKey;
+ }
+
+ public void setRoleKey(String roleKey)
+ {
+ this.roleKey = roleKey;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getRoleSort()
+ {
+ return roleSort;
+ }
+
+ public void setRoleSort(Integer roleSort)
+ {
+ this.roleSort = roleSort;
+ }
+
+ public String getDataScope()
+ {
+ return dataScope;
+ }
+
+ public void setDataScope(String dataScope)
+ {
+ this.dataScope = dataScope;
+ }
+
+ public boolean isMenuCheckStrictly()
+ {
+ return menuCheckStrictly;
+ }
+
+ public void setMenuCheckStrictly(boolean menuCheckStrictly)
+ {
+ this.menuCheckStrictly = menuCheckStrictly;
+ }
+
+ public boolean isDeptCheckStrictly()
+ {
+ return deptCheckStrictly;
+ }
+
+ public void setDeptCheckStrictly(boolean deptCheckStrictly)
+ {
+ this.deptCheckStrictly = deptCheckStrictly;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ public boolean isFlag()
+ {
+ return flag;
+ }
+
+ public void setFlag(boolean flag)
+ {
+ this.flag = flag;
+ }
+
+ public Long[] getMenuIds()
+ {
+ return menuIds;
+ }
+
+ public void setMenuIds(Long[] menuIds)
+ {
+ this.menuIds = menuIds;
+ }
+
+ public Long[] getDeptIds()
+ {
+ return deptIds;
+ }
+
+ public void setDeptIds(Long[] deptIds)
+ {
+ this.deptIds = deptIds;
+ }
+
+ public Set<String> getPermissions()
+ {
+ return permissions;
+ }
+
+ public void setPermissions(Set<String> permissions)
+ {
+ this.permissions = permissions;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("roleId", getRoleId())
+ .append("roleName", getRoleName())
+ .append("roleKey", getRoleKey())
+ .append("roleSort", getRoleSort())
+ .append("dataScope", getDataScope())
+ .append("menuCheckStrictly", isMenuCheckStrictly())
+ .append("deptCheckStrictly", isDeptCheckStrictly())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
new file mode 100644
index 0000000..25fc65d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -0,0 +1,324 @@
+package com.ruoyi.common.core.domain.entity;
+
+import java.util.Date;
+import java.util.List;
+import jakarta.validation.constraints.*;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.annotation.Excel.Type;
+import com.ruoyi.common.annotation.Excels;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.xss.Xss;
+
+/**
+ * 用户对象 sys_user
+ *
+ * @author ruoyi
+ */
+public class SysUser extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 用户ID */
+ @Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号")
+ private Long userId;
+
+ /** 部门ID */
+ @Excel(name = "部门编号", type = Type.IMPORT)
+ private Long deptId;
+
+ /** 用户账号 */
+ @Excel(name = "登录名称")
+ private String userName;
+
+ /** 用户昵称 */
+ @Excel(name = "用户名称")
+ private String nickName;
+
+ /** 用户邮箱 */
+ @Excel(name = "用户邮箱")
+ private String email;
+
+ /** 手机号码 */
+ @Excel(name = "手机号码", cellType = ColumnType.TEXT)
+ private String phonenumber;
+
+ /** 用户性别 */
+ @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
+ private String sex;
+
+ /** 用户头像 */
+ private String avatar;
+
+ /** 密码 */
+ private String password;
+
+ /** 帐号状态(0正常 1停用) */
+ @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /** 删除标志(0代表存在 2代表删除) */
+ private String delFlag;
+
+ /** 最后登录IP */
+ @Excel(name = "最后登录IP", type = Type.EXPORT)
+ private String loginIp;
+
+ /** 最后登录时间 */
+ @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
+ private Date loginDate;
+
+ /** 部门对象 */
+ @Excels({
+ @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
+ @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
+ })
+ private SysDept dept;
+
+ /** 角色对象 */
+ private List<SysRole> roles;
+
+ /** 角色组 */
+ private Long[] roleIds;
+
+ /** 岗位组 */
+ private Long[] postIds;
+
+ /** 角色ID */
+ private Long roleId;
+
+ public SysUser()
+ {
+
+ }
+
+ public SysUser(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public boolean isAdmin()
+ {
+ return isAdmin(this.userId);
+ }
+
+ public static boolean isAdmin(Long userId)
+ {
+ return userId != null && 1L == userId;
+ }
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ @Xss(message = "用户昵称不能包含脚本字符")
+ @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
+ public String getNickName()
+ {
+ return nickName;
+ }
+
+ public void setNickName(String nickName)
+ {
+ this.nickName = nickName;
+ }
+
+ @Xss(message = "用户账号不能包含脚本字符")
+ @NotBlank(message = "用户账号不能为空")
+ @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ public void setUserName(String userName)
+ {
+ this.userName = userName;
+ }
+
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
+ public String getEmail()
+ {
+ return email;
+ }
+
+ public void setEmail(String email)
+ {
+ this.email = email;
+ }
+
+ @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
+ public String getPhonenumber()
+ {
+ return phonenumber;
+ }
+
+ public void setPhonenumber(String phonenumber)
+ {
+ this.phonenumber = phonenumber;
+ }
+
+ public String getSex()
+ {
+ return sex;
+ }
+
+ public void setSex(String sex)
+ {
+ this.sex = sex;
+ }
+
+ public String getAvatar()
+ {
+ return avatar;
+ }
+
+ public void setAvatar(String avatar)
+ {
+ this.avatar = avatar;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public void setPassword(String password)
+ {
+ this.password = password;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ public String getLoginIp()
+ {
+ return loginIp;
+ }
+
+ public void setLoginIp(String loginIp)
+ {
+ this.loginIp = loginIp;
+ }
+
+ public Date getLoginDate()
+ {
+ return loginDate;
+ }
+
+ public void setLoginDate(Date loginDate)
+ {
+ this.loginDate = loginDate;
+ }
+
+ public SysDept getDept()
+ {
+ return dept;
+ }
+
+ public void setDept(SysDept dept)
+ {
+ this.dept = dept;
+ }
+
+ public List<SysRole> getRoles()
+ {
+ return roles;
+ }
+
+ public void setRoles(List<SysRole> roles)
+ {
+ this.roles = roles;
+ }
+
+ public Long[] getRoleIds()
+ {
+ return roleIds;
+ }
+
+ public void setRoleIds(Long[] roleIds)
+ {
+ this.roleIds = roleIds;
+ }
+
+ public Long[] getPostIds()
+ {
+ return postIds;
+ }
+
+ public void setPostIds(Long[] postIds)
+ {
+ this.postIds = postIds;
+ }
+
+ public Long getRoleId()
+ {
+ return roleId;
+ }
+
+ public void setRoleId(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("userId", getUserId())
+ .append("deptId", getDeptId())
+ .append("userName", getUserName())
+ .append("nickName", getNickName())
+ .append("email", getEmail())
+ .append("phonenumber", getPhonenumber())
+ .append("sex", getSex())
+ .append("avatar", getAvatar())
+ .append("password", getPassword())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("loginIp", getLoginIp())
+ .append("loginDate", getLoginDate())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .append("dept", getDept())
+ .toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java
new file mode 100644
index 0000000..b5bc8c8
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java
@@ -0,0 +1,69 @@
+package com.ruoyi.common.core.domain.model;
+
+/**
+ * 用户登录对象
+ *
+ * @author ruoyi
+ */
+public class LoginBody
+{
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * 用户密码
+ */
+ private String password;
+
+ /**
+ * 验证码
+ */
+ private String code;
+
+ /**
+ * 唯一标识
+ */
+ private String uuid;
+
+ public String getUsername()
+ {
+ return username;
+ }
+
+ public void setUsername(String username)
+ {
+ this.username = username;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public void setPassword(String password)
+ {
+ this.password = password;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public void setCode(String code)
+ {
+ this.code = code;
+ }
+
+ public String getUuid()
+ {
+ return uuid;
+ }
+
+ public void setUuid(String uuid)
+ {
+ this.uuid = uuid;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
new file mode 100644
index 0000000..670e6b3
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
@@ -0,0 +1,266 @@
+package com.ruoyi.common.core.domain.model;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author ruoyi
+ */
+public class LoginUser implements UserDetails
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户唯一标识
+ */
+ private String token;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地点
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 权限列表
+ */
+ private Set<String> permissions;
+
+ /**
+ * 用户信息
+ */
+ private SysUser user;
+
+ public LoginUser()
+ {
+ }
+
+ public LoginUser(SysUser user, Set<String> permissions)
+ {
+ this.user = user;
+ this.permissions = permissions;
+ }
+
+ public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
+ {
+ this.userId = userId;
+ this.deptId = deptId;
+ this.user = user;
+ this.permissions = permissions;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ public String getToken()
+ {
+ return token;
+ }
+
+ public void setToken(String token)
+ {
+ this.token = token;
+ }
+
+ @JSONField(serialize = false)
+ @Override
+ public String getPassword()
+ {
+ return user.getPassword();
+ }
+
+ @Override
+ public String getUsername()
+ {
+ return user.getUserName();
+ }
+
+ /**
+ * 账户是否未过期,过期无法验证
+ */
+ @JSONField(serialize = false)
+ @Override
+ public boolean isAccountNonExpired()
+ {
+ return true;
+ }
+
+ /**
+ * 指定用户是否解锁,锁定的用户无法进行身份验证
+ *
+ * @return
+ */
+ @JSONField(serialize = false)
+ @Override
+ public boolean isAccountNonLocked()
+ {
+ return true;
+ }
+
+ /**
+ * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
+ *
+ * @return
+ */
+ @JSONField(serialize = false)
+ @Override
+ public boolean isCredentialsNonExpired()
+ {
+ return true;
+ }
+
+ /**
+ * 是否可用 ,禁用的用户不能身份验证
+ *
+ * @return
+ */
+ @JSONField(serialize = false)
+ @Override
+ public boolean isEnabled()
+ {
+ return true;
+ }
+
+ public Long getLoginTime()
+ {
+ return loginTime;
+ }
+
+ public void setLoginTime(Long loginTime)
+ {
+ this.loginTime = loginTime;
+ }
+
+ public String getIpaddr()
+ {
+ return ipaddr;
+ }
+
+ public void setIpaddr(String ipaddr)
+ {
+ this.ipaddr = ipaddr;
+ }
+
+ public String getLoginLocation()
+ {
+ return loginLocation;
+ }
+
+ public void setLoginLocation(String loginLocation)
+ {
+ this.loginLocation = loginLocation;
+ }
+
+ public String getBrowser()
+ {
+ return browser;
+ }
+
+ public void setBrowser(String browser)
+ {
+ this.browser = browser;
+ }
+
+ public String getOs()
+ {
+ return os;
+ }
+
+ public void setOs(String os)
+ {
+ this.os = os;
+ }
+
+ public Long getExpireTime()
+ {
+ return expireTime;
+ }
+
+ public void setExpireTime(Long expireTime)
+ {
+ this.expireTime = expireTime;
+ }
+
+ public Set<String> getPermissions()
+ {
+ return permissions;
+ }
+
+ public void setPermissions(Set<String> permissions)
+ {
+ this.permissions = permissions;
+ }
+
+ public SysUser getUser()
+ {
+ return user;
+ }
+
+ public void setUser(SysUser user)
+ {
+ this.user = user;
+ }
+
+ @Override
+ public Collection<? extends GrantedAuthority> getAuthorities()
+ {
+ return null;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java
new file mode 100644
index 0000000..868a1fc
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java
@@ -0,0 +1,11 @@
+package com.ruoyi.common.core.domain.model;
+
+/**
+ * 用户注册对象
+ *
+ * @author ruoyi
+ */
+public class RegisterBody extends LoginBody
+{
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java
new file mode 100644
index 0000000..8966cb4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java
@@ -0,0 +1,101 @@
+package com.ruoyi.common.core.page;
+
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 分页数据
+ *
+ * @author ruoyi
+ */
+public class PageDomain
+{
+ /** 当前记录起始索引 */
+ private Integer pageNum;
+
+ /** 每页显示记录数 */
+ private Integer pageSize;
+
+ /** 排序列 */
+ private String orderByColumn;
+
+ /** 排序的方向desc或者asc */
+ private String isAsc = "asc";
+
+ /** 分页参数合理化 */
+ private Boolean reasonable = true;
+
+ public String getOrderBy()
+ {
+ if (StringUtils.isEmpty(orderByColumn))
+ {
+ return "";
+ }
+ return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc;
+ }
+
+ public Integer getPageNum()
+ {
+ return pageNum;
+ }
+
+ public void setPageNum(Integer pageNum)
+ {
+ this.pageNum = pageNum;
+ }
+
+ public Integer getPageSize()
+ {
+ return pageSize;
+ }
+
+ public void setPageSize(Integer pageSize)
+ {
+ this.pageSize = pageSize;
+ }
+
+ public String getOrderByColumn()
+ {
+ return orderByColumn;
+ }
+
+ public void setOrderByColumn(String orderByColumn)
+ {
+ this.orderByColumn = orderByColumn;
+ }
+
+ public String getIsAsc()
+ {
+ return isAsc;
+ }
+
+ public void setIsAsc(String isAsc)
+ {
+ if (StringUtils.isNotEmpty(isAsc))
+ {
+ // 兼容前端排序类型
+ if ("ascending".equals(isAsc))
+ {
+ isAsc = "asc";
+ }
+ else if ("descending".equals(isAsc))
+ {
+ isAsc = "desc";
+ }
+ this.isAsc = isAsc;
+ }
+ }
+
+ public Boolean getReasonable()
+ {
+ if (StringUtils.isNull(reasonable))
+ {
+ return Boolean.TRUE;
+ }
+ return reasonable;
+ }
+
+ public void setReasonable(Boolean reasonable)
+ {
+ this.reasonable = reasonable;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java
new file mode 100644
index 0000000..847685b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java
@@ -0,0 +1,85 @@
+package com.ruoyi.common.core.page;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 表格分页数据对象
+ *
+ * @author ruoyi
+ */
+public class TableDataInfo implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 总记录数 */
+ private long total;
+
+ /** 列表数据 */
+ private List<?> rows;
+
+ /** 消息状态码 */
+ private int code;
+
+ /** 消息内容 */
+ private String msg;
+
+ /**
+ * 表格数据对象
+ */
+ public TableDataInfo()
+ {
+ }
+
+ /**
+ * 分页
+ *
+ * @param list 列表数据
+ * @param total 总记录数
+ */
+ public TableDataInfo(List<?> list, int total)
+ {
+ this.rows = list;
+ this.total = total;
+ }
+
+ public long getTotal()
+ {
+ return total;
+ }
+
+ public void setTotal(long total)
+ {
+ this.total = total;
+ }
+
+ public List<?> getRows()
+ {
+ return rows;
+ }
+
+ public void setRows(List<?> rows)
+ {
+ this.rows = rows;
+ }
+
+ public int getCode()
+ {
+ return code;
+ }
+
+ public void setCode(int code)
+ {
+ this.code = code;
+ }
+
+ public String getMsg()
+ {
+ return msg;
+ }
+
+ public void setMsg(String msg)
+ {
+ this.msg = msg;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java
new file mode 100644
index 0000000..8cfecf0
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java
@@ -0,0 +1,56 @@
+package com.ruoyi.common.core.page;
+
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 表格数据处理
+ *
+ * @author ruoyi
+ */
+public class TableSupport
+{
+ /**
+ * 当前记录起始索引
+ */
+ public static final String PAGE_NUM = "current";
+
+ /**
+ * 每页显示记录数
+ */
+ public static final String PAGE_SIZE = "pageSize";
+
+ /**
+ * 排序列
+ */
+ public static final String ORDER_BY_COLUMN = "orderByColumn";
+
+ /**
+ * 排序的方向 "desc" 或者 "asc".
+ */
+ public static final String IS_ASC = "isAsc";
+
+ /**
+ * 分页参数合理化
+ */
+ public static final String REASONABLE = "reasonable";
+
+ /**
+ * 封装分页对象
+ */
+ public static PageDomain getPageDomain()
+ {
+ PageDomain pageDomain = new PageDomain();
+ pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
+ pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
+ pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
+ pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
+ pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
+ return pageDomain;
+ }
+
+ public static PageDomain buildPageRequest()
+ {
+ return getPageDomain();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java
new file mode 100644
index 0000000..44e80d8
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java
@@ -0,0 +1,268 @@
+package com.ruoyi.common.core.redis;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring redis 工具类
+ *
+ * @author ruoyi
+ **/
+@SuppressWarnings(value = { "unchecked", "rawtypes" })
+@Component
+public class RedisCache
+{
+ @Autowired
+ public RedisTemplate redisTemplate;
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ */
+ public <T> void setCacheObject(final String key, final T value)
+ {
+ redisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ * @param timeout 时间
+ * @param timeUnit 时间颗粒度
+ */
+ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
+ {
+ redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire(final String key, final long timeout)
+ {
+ return expire(key, timeout, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ * @param unit 时间单位
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire(final String key, final long timeout, final TimeUnit unit)
+ {
+ return redisTemplate.expire(key, timeout, unit);
+ }
+
+ /**
+ * 获取有效时间
+ *
+ * @param key Redis键
+ * @return 有效时间
+ */
+ public long getExpire(final String key)
+ {
+ return redisTemplate.getExpire(key);
+ }
+
+ /**
+ * 判断 key是否存在
+ *
+ * @param key 键
+ * @return true 存在 false不存在
+ */
+ public Boolean hasKey(String key)
+ {
+ return redisTemplate.hasKey(key);
+ }
+
+ /**
+ * 获得缓存的基本对象。
+ *
+ * @param key 缓存键值
+ * @return 缓存键值对应的数据
+ */
+ public <T> T getCacheObject(final String key)
+ {
+ ValueOperations<String, T> operation = redisTemplate.opsForValue();
+ return operation.get(key);
+ }
+
+ /**
+ * 删除单个对象
+ *
+ * @param key
+ */
+ public boolean deleteObject(final String key)
+ {
+ return redisTemplate.delete(key);
+ }
+
+ /**
+ * 删除集合对象
+ *
+ * @param collection 多个对象
+ * @return
+ */
+ public boolean deleteObject(final Collection collection)
+ {
+ return redisTemplate.delete(collection) > 0;
+ }
+
+ /**
+ * 缓存List数据
+ *
+ * @param key 缓存的键值
+ * @param dataList 待缓存的List数据
+ * @return 缓存的对象
+ */
+ public <T> long setCacheList(final String key, final List<T> dataList)
+ {
+ Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 获得缓存的list对象
+ *
+ * @param key 缓存的键值
+ * @return 缓存键值对应的数据
+ */
+ public <T> List<T> getCacheList(final String key)
+ {
+ return redisTemplate.opsForList().range(key, 0, -1);
+ }
+
+ /**
+ * 缓存Set
+ *
+ * @param key 缓存键值
+ * @param dataSet 缓存的数据
+ * @return 缓存数据的对象
+ */
+ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
+ {
+ BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
+ Iterator<T> it = dataSet.iterator();
+ while (it.hasNext())
+ {
+ setOperation.add(it.next());
+ }
+ return setOperation;
+ }
+
+ /**
+ * 获得缓存的set
+ *
+ * @param key
+ * @return
+ */
+ public <T> Set<T> getCacheSet(final String key)
+ {
+ return redisTemplate.opsForSet().members(key);
+ }
+
+ /**
+ * 缓存Map
+ *
+ * @param key
+ * @param dataMap
+ */
+ public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
+ {
+ if (dataMap != null) {
+ redisTemplate.opsForHash().putAll(key, dataMap);
+ }
+ }
+
+ /**
+ * 获得缓存的Map
+ *
+ * @param key
+ * @return
+ */
+ public <T> Map<String, T> getCacheMap(final String key)
+ {
+ return redisTemplate.opsForHash().entries(key);
+ }
+
+ /**
+ * 往Hash中存入数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @param value 值
+ */
+ public <T> void setCacheMapValue(final String key, final String hKey, final T value)
+ {
+ redisTemplate.opsForHash().put(key, hKey, value);
+ }
+
+ /**
+ * 获取Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @return Hash中的对象
+ */
+ public <T> T getCacheMapValue(final String key, final String hKey)
+ {
+ HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
+ return opsForHash.get(key, hKey);
+ }
+
+ /**
+ * 获取多个Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键集合
+ * @return Hash对象集合
+ */
+ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
+ {
+ return redisTemplate.opsForHash().multiGet(key, hKeys);
+ }
+
+ /**
+ * 删除Hash中的某条数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @return 是否成功
+ */
+ public boolean deleteCacheMapValue(final String key, final String hKey)
+ {
+ return redisTemplate.opsForHash().delete(key, hKey) > 0;
+ }
+
+ /**
+ * 获得缓存的基本对象列表
+ *
+ * @param pattern 字符串前缀
+ * @return 对象列表
+ */
+ public Collection<String> keys(final String pattern)
+ {
+ return redisTemplate.keys(pattern);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java
new file mode 100644
index 0000000..84124aa
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java
@@ -0,0 +1,86 @@
+package com.ruoyi.common.core.text;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 字符集工具类
+ *
+ * @author ruoyi
+ */
+public class CharsetKit
+{
+ /** ISO-8859-1 */
+ public static final String ISO_8859_1 = "ISO-8859-1";
+ /** UTF-8 */
+ public static final String UTF_8 = "UTF-8";
+ /** GBK */
+ public static final String GBK = "GBK";
+
+ /** ISO-8859-1 */
+ public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
+ /** UTF-8 */
+ public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
+ /** GBK */
+ public static final Charset CHARSET_GBK = Charset.forName(GBK);
+
+ /**
+ * 转换为Charset对象
+ *
+ * @param charset 字符集,为空则返回默认字符集
+ * @return Charset
+ */
+ public static Charset charset(String charset)
+ {
+ return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
+ }
+
+ /**
+ * 转换字符串的字符集编码
+ *
+ * @param source 字符串
+ * @param srcCharset 源字符集,默认ISO-8859-1
+ * @param destCharset 目标字符集,默认UTF-8
+ * @return 转换后的字符集
+ */
+ public static String convert(String source, String srcCharset, String destCharset)
+ {
+ return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
+ }
+
+ /**
+ * 转换字符串的字符集编码
+ *
+ * @param source 字符串
+ * @param srcCharset 源字符集,默认ISO-8859-1
+ * @param destCharset 目标字符集,默认UTF-8
+ * @return 转换后的字符集
+ */
+ public static String convert(String source, Charset srcCharset, Charset destCharset)
+ {
+ if (null == srcCharset)
+ {
+ srcCharset = StandardCharsets.ISO_8859_1;
+ }
+
+ if (null == destCharset)
+ {
+ destCharset = StandardCharsets.UTF_8;
+ }
+
+ if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
+ {
+ return source;
+ }
+ return new String(source.getBytes(srcCharset), destCharset);
+ }
+
+ /**
+ * @return 系统字符集编码
+ */
+ public static String systemCharset()
+ {
+ return Charset.defaultCharset().name();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java
new file mode 100644
index 0000000..edf9afc
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java
@@ -0,0 +1,1010 @@
+package com.ruoyi.common.core.text;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
+import java.util.Set;
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * 类型转换器
+ *
+ * @author ruoyi
+ */
+public class Convert
+{
+ /**
+ * 转换为字符串<br>
+ * 如果给定的值为null,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static String toStr(Object value, String defaultValue)
+ {
+ if (null == value)
+ {
+ return defaultValue;
+ }
+ if (value instanceof String)
+ {
+ return (String) value;
+ }
+ return value.toString();
+ }
+
+ /**
+ * 转换为字符串<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static String toStr(Object value)
+ {
+ return toStr(value, null);
+ }
+
+ /**
+ * 转换为字符<br>
+ * 如果给定的值为null,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Character toChar(Object value, Character defaultValue)
+ {
+ if (null == value)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Character)
+ {
+ return (Character) value;
+ }
+
+ final String valueStr = toStr(value, null);
+ return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0);
+ }
+
+ /**
+ * 转换为字符<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Character toChar(Object value)
+ {
+ return toChar(value, null);
+ }
+
+ /**
+ * 转换为byte<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Byte toByte(Object value, Byte defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Byte)
+ {
+ return (Byte) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).byteValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return Byte.parseByte(valueStr);
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为byte<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Byte toByte(Object value)
+ {
+ return toByte(value, null);
+ }
+
+ /**
+ * 转换为Short<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Short toShort(Object value, Short defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Short)
+ {
+ return (Short) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).shortValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return Short.parseShort(valueStr.trim());
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Short<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Short toShort(Object value)
+ {
+ return toShort(value, null);
+ }
+
+ /**
+ * 转换为Number<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Number toNumber(Object value, Number defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Number)
+ {
+ return (Number) value;
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return NumberFormat.getInstance().parse(valueStr);
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Number<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Number toNumber(Object value)
+ {
+ return toNumber(value, null);
+ }
+
+ /**
+ * 转换为int<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Integer toInt(Object value, Integer defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Integer)
+ {
+ return (Integer) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).intValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return Integer.parseInt(valueStr.trim());
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为int<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Integer toInt(Object value)
+ {
+ return toInt(value, null);
+ }
+
+ /**
+ * 转换为Integer数组<br>
+ *
+ * @param str 被转换的值
+ * @return 结果
+ */
+ public static Integer[] toIntArray(String str)
+ {
+ return toIntArray(",", str);
+ }
+
+ /**
+ * 转换为Long数组<br>
+ *
+ * @param str 被转换的值
+ * @return 结果
+ */
+ public static Long[] toLongArray(String str)
+ {
+ return toLongArray(",", str);
+ }
+
+ /**
+ * 转换为Integer数组<br>
+ *
+ * @param split 分隔符
+ * @param split 被转换的值
+ * @return 结果
+ */
+ public static Integer[] toIntArray(String split, String str)
+ {
+ if (StringUtils.isEmpty(str))
+ {
+ return new Integer[] {};
+ }
+ String[] arr = str.split(split);
+ final Integer[] ints = new Integer[arr.length];
+ for (int i = 0; i < arr.length; i++)
+ {
+ final Integer v = toInt(arr[i], 0);
+ ints[i] = v;
+ }
+ return ints;
+ }
+
+ /**
+ * 转换为Long数组<br>
+ *
+ * @param split 分隔符
+ * @param str 被转换的值
+ * @return 结果
+ */
+ public static Long[] toLongArray(String split, String str)
+ {
+ if (StringUtils.isEmpty(str))
+ {
+ return new Long[] {};
+ }
+ String[] arr = str.split(split);
+ final Long[] longs = new Long[arr.length];
+ for (int i = 0; i < arr.length; i++)
+ {
+ final Long v = toLong(arr[i], null);
+ longs[i] = v;
+ }
+ return longs;
+ }
+
+ /**
+ * 转换为String数组<br>
+ *
+ * @param str 被转换的值
+ * @return 结果
+ */
+ public static String[] toStrArray(String str)
+ {
+ if (StringUtils.isEmpty(str))
+ {
+ return new String[] {};
+ }
+ return toStrArray(",", str);
+ }
+
+ /**
+ * 转换为String数组<br>
+ *
+ * @param split 分隔符
+ * @param split 被转换的值
+ * @return 结果
+ */
+ public static String[] toStrArray(String split, String str)
+ {
+ return str.split(split);
+ }
+
+ /**
+ * 转换为long<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Long toLong(Object value, Long defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Long)
+ {
+ return (Long) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).longValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ // 支持科学计数法
+ return new BigDecimal(valueStr.trim()).longValue();
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为long<br>
+ * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Long toLong(Object value)
+ {
+ return toLong(value, null);
+ }
+
+ /**
+ * 转换为double<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Double toDouble(Object value, Double defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Double)
+ {
+ return (Double) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).doubleValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ // 支持科学计数法
+ return new BigDecimal(valueStr.trim()).doubleValue();
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为double<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Double toDouble(Object value)
+ {
+ return toDouble(value, null);
+ }
+
+ /**
+ * 转换为Float<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Float toFloat(Object value, Float defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Float)
+ {
+ return (Float) value;
+ }
+ if (value instanceof Number)
+ {
+ return ((Number) value).floatValue();
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return Float.parseFloat(valueStr.trim());
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Float<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Float toFloat(Object value)
+ {
+ return toFloat(value, null);
+ }
+
+ /**
+ * 转换为boolean<br>
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static Boolean toBool(Object value, Boolean defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof Boolean)
+ {
+ return (Boolean) value;
+ }
+ String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ valueStr = valueStr.trim().toLowerCase();
+ switch (valueStr)
+ {
+ case "true":
+ case "yes":
+ case "ok":
+ case "1":
+ return true;
+ case "false":
+ case "no":
+ case "0":
+ return false;
+ default:
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为boolean<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static Boolean toBool(Object value)
+ {
+ return toBool(value, null);
+ }
+
+ /**
+ * 转换为Enum对象<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ *
+ * @param clazz Enum的Class
+ * @param value 值
+ * @param defaultValue 默认值
+ * @return Enum
+ */
+ public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value, E defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (clazz.isAssignableFrom(value.getClass()))
+ {
+ @SuppressWarnings("unchecked")
+ E myE = (E) value;
+ return myE;
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return Enum.valueOf(clazz, valueStr);
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为Enum对象<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ *
+ * @param clazz Enum的Class
+ * @param value 值
+ * @return Enum
+ */
+ public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value)
+ {
+ return toEnum(clazz, value, null);
+ }
+
+ /**
+ * 转换为BigInteger<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static BigInteger toBigInteger(Object value, BigInteger defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof BigInteger)
+ {
+ return (BigInteger) value;
+ }
+ if (value instanceof Long)
+ {
+ return BigInteger.valueOf((Long) value);
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return new BigInteger(valueStr);
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为BigInteger<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static BigInteger toBigInteger(Object value)
+ {
+ return toBigInteger(value, null);
+ }
+
+ /**
+ * 转换为BigDecimal<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @param defaultValue 转换错误时的默认值
+ * @return 结果
+ */
+ public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue)
+ {
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ if (value instanceof BigDecimal)
+ {
+ return (BigDecimal) value;
+ }
+ if (value instanceof Long)
+ {
+ return new BigDecimal((Long) value);
+ }
+ if (value instanceof Double)
+ {
+ return BigDecimal.valueOf((Double) value);
+ }
+ if (value instanceof Integer)
+ {
+ return new BigDecimal((Integer) value);
+ }
+ final String valueStr = toStr(value, null);
+ if (StringUtils.isEmpty(valueStr))
+ {
+ return defaultValue;
+ }
+ try
+ {
+ return new BigDecimal(valueStr);
+ }
+ catch (Exception e)
+ {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * 转换为BigDecimal<br>
+ * 如果给定的值为空,或者转换失败,返回默认值<br>
+ * 转换失败不会报错
+ *
+ * @param value 被转换的值
+ * @return 结果
+ */
+ public static BigDecimal toBigDecimal(Object value)
+ {
+ return toBigDecimal(value, null);
+ }
+
+ /**
+ * 将对象转为字符串<br>
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ * @return 字符串
+ */
+ public static String utf8Str(Object obj)
+ {
+ return str(obj, CharsetKit.CHARSET_UTF_8);
+ }
+
+ /**
+ * 将对象转为字符串<br>
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ * @param charsetName 字符集
+ * @return 字符串
+ */
+ public static String str(Object obj, String charsetName)
+ {
+ return str(obj, Charset.forName(charsetName));
+ }
+
+ /**
+ * 将对象转为字符串<br>
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+ *
+ * @param obj 对象
+ * @param charset 字符集
+ * @return 字符串
+ */
+ public static String str(Object obj, Charset charset)
+ {
+ if (null == obj)
+ {
+ return null;
+ }
+
+ if (obj instanceof String)
+ {
+ return (String) obj;
+ }
+ else if (obj instanceof byte[])
+ {
+ return str((byte[]) obj, charset);
+ }
+ else if (obj instanceof Byte[])
+ {
+ byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj);
+ return str(bytes, charset);
+ }
+ else if (obj instanceof ByteBuffer)
+ {
+ return str((ByteBuffer) obj, charset);
+ }
+ return obj.toString();
+ }
+
+ /**
+ * 将byte数组转为字符串
+ *
+ * @param bytes byte数组
+ * @param charset 字符集
+ * @return 字符串
+ */
+ public static String str(byte[] bytes, String charset)
+ {
+ return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset));
+ }
+
+ /**
+ * 解码字节码
+ *
+ * @param data 字符串
+ * @param charset 字符集,如果此字段为空,则解码的结果取决于平台
+ * @return 解码后的字符串
+ */
+ public static String str(byte[] data, Charset charset)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ if (null == charset)
+ {
+ return new String(data);
+ }
+ return new String(data, charset);
+ }
+
+ /**
+ * 将编码的byteBuffer数据转换为字符串
+ *
+ * @param data 数据
+ * @param charset 字符集,如果为空使用当前系统字符集
+ * @return 字符串
+ */
+ public static String str(ByteBuffer data, String charset)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ return str(data, Charset.forName(charset));
+ }
+
+ /**
+ * 将编码的byteBuffer数据转换为字符串
+ *
+ * @param data 数据
+ * @param charset 字符集,如果为空使用当前系统字符集
+ * @return 字符串
+ */
+ public static String str(ByteBuffer data, Charset charset)
+ {
+ if (null == charset)
+ {
+ charset = Charset.defaultCharset();
+ }
+ return charset.decode(data).toString();
+ }
+
+ // ----------------------------------------------------------------------- 全角半角转换
+ /**
+ * 半角转全角
+ *
+ * @param input String.
+ * @return 全角字符串.
+ */
+ public static String toSBC(String input)
+ {
+ return toSBC(input, null);
+ }
+
+ /**
+ * 半角转全角
+ *
+ * @param input String
+ * @param notConvertSet 不替换的字符集合
+ * @return 全角字符串.
+ */
+ public static String toSBC(String input, Set<Character> notConvertSet)
+ {
+ char[] c = input.toCharArray();
+ for (int i = 0; i < c.length; i++)
+ {
+ if (null != notConvertSet && notConvertSet.contains(c[i]))
+ {
+ // 跳过不替换的字符
+ continue;
+ }
+
+ if (c[i] == ' ')
+ {
+ c[i] = '\u3000';
+ }
+ else if (c[i] < '\177')
+ {
+ c[i] = (char) (c[i] + 65248);
+
+ }
+ }
+ return new String(c);
+ }
+
+ /**
+ * 全角转半角
+ *
+ * @param input String.
+ * @return 半角字符串
+ */
+ public static String toDBC(String input)
+ {
+ return toDBC(input, null);
+ }
+
+ /**
+ * 替换全角为半角
+ *
+ * @param text 文本
+ * @param notConvertSet 不替换的字符集合
+ * @return 替换后的字符
+ */
+ public static String toDBC(String text, Set<Character> notConvertSet)
+ {
+ char[] c = text.toCharArray();
+ for (int i = 0; i < c.length; i++)
+ {
+ if (null != notConvertSet && notConvertSet.contains(c[i]))
+ {
+ // 跳过不替换的字符
+ continue;
+ }
+
+ if (c[i] == '\u3000')
+ {
+ c[i] = ' ';
+ }
+ else if (c[i] > '\uFF00' && c[i] < '\uFF5F')
+ {
+ c[i] = (char) (c[i] - 65248);
+ }
+ }
+ String returnString = new String(c);
+
+ return returnString;
+ }
+
+ /**
+ * 数字金额大写转换 先写个完整的然后将如零拾替换成零
+ *
+ * @param n 数字
+ * @return 中文大写数字
+ */
+ public static String digitUppercase(double n)
+ {
+ String[] fraction = { "角", "分" };
+ String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
+ String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } };
+
+ String head = n < 0 ? "负" : "";
+ n = Math.abs(n);
+
+ String s = "";
+ for (int i = 0; i < fraction.length; i++)
+ {
+ // 优化double计算精度丢失问题
+ BigDecimal nNum = new BigDecimal(n);
+ BigDecimal decimal = new BigDecimal(10);
+ BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN);
+ double d = scale.doubleValue();
+ s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", "");
+ }
+ if (s.length() < 1)
+ {
+ s = "整";
+ }
+ int integerPart = (int) Math.floor(n);
+
+ for (int i = 0; i < unit[0].length && integerPart > 0; i++)
+ {
+ String p = "";
+ for (int j = 0; j < unit[1].length && n > 0; j++)
+ {
+ p = digit[integerPart % 10] + unit[1][j] + p;
+ integerPart = integerPart / 10;
+ }
+ s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
+ }
+ return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java
new file mode 100644
index 0000000..c78ac77
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java
@@ -0,0 +1,92 @@
+package com.ruoyi.common.core.text;
+
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 字符串格式化
+ *
+ * @author ruoyi
+ */
+public class StrFormatter
+{
+ public static final String EMPTY_JSON = "{}";
+ public static final char C_BACKSLASH = '\\';
+ public static final char C_DELIM_START = '{';
+ public static final char C_DELIM_END = '}';
+
+ /**
+ * 格式化字符串<br>
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+ * 例:<br>
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+ *
+ * @param strPattern 字符串模板
+ * @param argArray 参数列表
+ * @return 结果
+ */
+ public static String format(final String strPattern, final Object... argArray)
+ {
+ if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray))
+ {
+ return strPattern;
+ }
+ final int strPatternLength = strPattern.length();
+
+ // 初始化定义好的长度以获得更好的性能
+ StringBuilder sbuf = new StringBuilder(strPatternLength + 50);
+
+ int handledPosition = 0;
+ int delimIndex;// 占位符所在位置
+ for (int argIndex = 0; argIndex < argArray.length; argIndex++)
+ {
+ delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
+ if (delimIndex == -1)
+ {
+ if (handledPosition == 0)
+ {
+ return strPattern;
+ }
+ else
+ { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
+ sbuf.append(strPattern, handledPosition, strPatternLength);
+ return sbuf.toString();
+ }
+ }
+ else
+ {
+ if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH)
+ {
+ if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH)
+ {
+ // 转义符之前还有一个转义符,占位符依旧有效
+ sbuf.append(strPattern, handledPosition, delimIndex - 1);
+ sbuf.append(Convert.utf8Str(argArray[argIndex]));
+ handledPosition = delimIndex + 2;
+ }
+ else
+ {
+ // 占位符被转义
+ argIndex--;
+ sbuf.append(strPattern, handledPosition, delimIndex - 1);
+ sbuf.append(C_DELIM_START);
+ handledPosition = delimIndex + 1;
+ }
+ }
+ else
+ {
+ // 正常占位符
+ sbuf.append(strPattern, handledPosition, delimIndex);
+ sbuf.append(Convert.utf8Str(argArray[argIndex]));
+ handledPosition = delimIndex + 2;
+ }
+ }
+ }
+ // 加入最后一个占位符后所有的字符
+ sbuf.append(strPattern, handledPosition, strPattern.length());
+
+ return sbuf.toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java
new file mode 100644
index 0000000..10b7306
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java
@@ -0,0 +1,20 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 操作状态
+ *
+ * @author ruoyi
+ *
+ */
+public enum BusinessStatus
+{
+ /**
+ * 成功
+ */
+ SUCCESS,
+
+ /**
+ * 失败
+ */
+ FAIL,
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java
new file mode 100644
index 0000000..2e17c4a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java
@@ -0,0 +1,59 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 业务操作类型
+ *
+ * @author ruoyi
+ */
+public enum BusinessType
+{
+ /**
+ * 其它
+ */
+ OTHER,
+
+ /**
+ * 新增
+ */
+ INSERT,
+
+ /**
+ * 修改
+ */
+ UPDATE,
+
+ /**
+ * 删除
+ */
+ DELETE,
+
+ /**
+ * 授权
+ */
+ GRANT,
+
+ /**
+ * 导出
+ */
+ EXPORT,
+
+ /**
+ * 导入
+ */
+ IMPORT,
+
+ /**
+ * 强退
+ */
+ FORCE,
+
+ /**
+ * 生成代码
+ */
+ GENCODE,
+
+ /**
+ * 清空数据
+ */
+ CLEAN,
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java
new file mode 100644
index 0000000..0d945be
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java
@@ -0,0 +1,19 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 数据源
+ *
+ * @author ruoyi
+ */
+public enum DataSourceType
+{
+ /**
+ * 主库
+ */
+ MASTER,
+
+ /**
+ * 从库
+ */
+ SLAVE
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java
new file mode 100644
index 0000000..4508122
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java
@@ -0,0 +1,59 @@
+package com.ruoyi.common.enums;
+
+import java.util.function.Function;
+import com.ruoyi.common.utils.DesensitizedUtil;
+
+/**
+ * 脱敏类型
+ *
+ * @author ruoyi
+ */
+public enum DesensitizedType
+{
+ /**
+ * 姓名,第2位星号替换
+ */
+ USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
+
+ /**
+ * 密码,全部字符都用*代替
+ */
+ PASSWORD(DesensitizedUtil::password),
+
+ /**
+ * 身份证,中间10位星号替换
+ */
+ ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1** **** ****$2")),
+
+ /**
+ * 手机号,中间4位星号替换
+ */
+ PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
+
+ /**
+ * 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换
+ */
+ EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")),
+
+ /**
+ * 银行卡号,保留最后4位,其他星号替换
+ */
+ BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")),
+
+ /**
+ * 车牌号码,包含普通车辆、新能源车辆
+ */
+ CAR_LICENSE(DesensitizedUtil::carLicense);
+
+ private final Function<String, String> desensitizer;
+
+ DesensitizedType(Function<String, String> desensitizer)
+ {
+ this.desensitizer = desensitizer;
+ }
+
+ public Function<String, String> desensitizer()
+ {
+ return desensitizer;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java
new file mode 100644
index 0000000..be6f739
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java
@@ -0,0 +1,36 @@
+package com.ruoyi.common.enums;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.springframework.lang.Nullable;
+
+/**
+ * 请求方式
+ *
+ * @author ruoyi
+ */
+public enum HttpMethod
+{
+ GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
+
+ private static final Map<String, HttpMethod> mappings = new HashMap<>(16);
+
+ static
+ {
+ for (HttpMethod httpMethod : values())
+ {
+ mappings.put(httpMethod.name(), httpMethod);
+ }
+ }
+
+ @Nullable
+ public static HttpMethod resolve(@Nullable String method)
+ {
+ return (method != null ? mappings.get(method) : null);
+ }
+
+ public boolean matches(String method)
+ {
+ return (this == resolve(method));
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java
new file mode 100644
index 0000000..c609fd8
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java
@@ -0,0 +1,20 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 限流类型
+ *
+ * @author ruoyi
+ */
+
+public enum LimitType
+{
+ /**
+ * 默认策略全局限流
+ */
+ DEFAULT,
+
+ /**
+ * 根据请求者IP进行限流
+ */
+ IP
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java
new file mode 100644
index 0000000..bdd143c
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 操作人类别
+ *
+ * @author ruoyi
+ */
+public enum OperatorType
+{
+ /**
+ * 其它
+ */
+ OTHER,
+
+ /**
+ * 后台用户
+ */
+ MANAGE,
+
+ /**
+ * 手机端用户
+ */
+ MOBILE
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java
new file mode 100644
index 0000000..d7ff44a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java
@@ -0,0 +1,30 @@
+package com.ruoyi.common.enums;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+public enum UserStatus
+{
+ OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+ UserStatus(String code, String info)
+ {
+ this.code = code;
+ this.info = info;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public String getInfo()
+ {
+ return info;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java
new file mode 100644
index 0000000..f6ad2ab
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java
@@ -0,0 +1,15 @@
+package com.ruoyi.common.exception;
+
+/**
+ * 演示模式异常
+ *
+ * @author ruoyi
+ */
+public class DemoModeException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ public DemoModeException()
+ {
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java
new file mode 100644
index 0000000..81a71b5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java
@@ -0,0 +1,58 @@
+package com.ruoyi.common.exception;
+
+/**
+ * 全局异常
+ *
+ * @author ruoyi
+ */
+public class GlobalException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ *
+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public GlobalException()
+ {
+ }
+
+ public GlobalException(String message)
+ {
+ this.message = message;
+ }
+
+ public String getDetailMessage()
+ {
+ return detailMessage;
+ }
+
+ public GlobalException setDetailMessage(String detailMessage)
+ {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+
+ @Override
+ public String getMessage()
+ {
+ return message;
+ }
+
+ public GlobalException setMessage(String message)
+ {
+ this.message = message;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java
new file mode 100644
index 0000000..fcc7ab6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java
@@ -0,0 +1,74 @@
+package com.ruoyi.common.exception;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+public final class ServiceException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ *
+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+ */
+ private String detailMessage;
+
+ /**
+ * 空构造方法,避免反序列化问题
+ */
+ public ServiceException()
+ {
+ }
+
+ public ServiceException(String message)
+ {
+ this.message = message;
+ }
+
+ public ServiceException(String message, Integer code)
+ {
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getDetailMessage()
+ {
+ return detailMessage;
+ }
+
+ @Override
+ public String getMessage()
+ {
+ return message;
+ }
+
+ public Integer getCode()
+ {
+ return code;
+ }
+
+ public ServiceException setMessage(String message)
+ {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage)
+ {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java
new file mode 100644
index 0000000..980fa46
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java
@@ -0,0 +1,26 @@
+package com.ruoyi.common.exception;
+
+/**
+ * 工具类异常
+ *
+ * @author ruoyi
+ */
+public class UtilException extends RuntimeException
+{
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public UtilException(Throwable e)
+ {
+ super(e.getMessage(), e);
+ }
+
+ public UtilException(String message)
+ {
+ super(message);
+ }
+
+ public UtilException(String message, Throwable throwable)
+ {
+ super(message, throwable);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java
new file mode 100644
index 0000000..b55d72e
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java
@@ -0,0 +1,97 @@
+package com.ruoyi.common.exception.base;
+
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 基础异常
+ *
+ * @author ruoyi
+ */
+public class BaseException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 所属模块
+ */
+ private String module;
+
+ /**
+ * 错误码
+ */
+ private String code;
+
+ /**
+ * 错误码对应的参数
+ */
+ private Object[] args;
+
+ /**
+ * 错误消息
+ */
+ private String defaultMessage;
+
+ public BaseException(String module, String code, Object[] args, String defaultMessage)
+ {
+ this.module = module;
+ this.code = code;
+ this.args = args;
+ this.defaultMessage = defaultMessage;
+ }
+
+ public BaseException(String module, String code, Object[] args)
+ {
+ this(module, code, args, null);
+ }
+
+ public BaseException(String module, String defaultMessage)
+ {
+ this(module, null, null, defaultMessage);
+ }
+
+ public BaseException(String code, Object[] args)
+ {
+ this(null, code, args, null);
+ }
+
+ public BaseException(String defaultMessage)
+ {
+ this(null, null, null, defaultMessage);
+ }
+
+ @Override
+ public String getMessage()
+ {
+ String message = null;
+ if (!StringUtils.isEmpty(code))
+ {
+ message = MessageUtils.message(code, args);
+ }
+ if (message == null)
+ {
+ message = defaultMessage;
+ }
+ return message;
+ }
+
+ public String getModule()
+ {
+ return module;
+ }
+
+ public String getCode()
+ {
+ return code;
+ }
+
+ public Object[] getArgs()
+ {
+ return args;
+ }
+
+ public String getDefaultMessage()
+ {
+ return defaultMessage;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java
new file mode 100644
index 0000000..871f09b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java
@@ -0,0 +1,19 @@
+package com.ruoyi.common.exception.file;
+
+import com.ruoyi.common.exception.base.BaseException;
+
+/**
+ * 文件信息异常类
+ *
+ * @author ruoyi
+ */
+public class FileException extends BaseException
+{
+ private static final long serialVersionUID = 1L;
+
+ public FileException(String code, Object[] args)
+ {
+ super("file", code, args, null);
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 0000000..70e0ec9
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.file;
+
+/**
+ * 文件名称超长限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileNameLengthLimitExceededException extends FileException
+{
+ private static final long serialVersionUID = 1L;
+
+ public FileNameLengthLimitExceededException(int defaultFileNameLength)
+ {
+ super("upload.filename.exceed.length", new Object[] { defaultFileNameLength });
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 0000000..ec6ab05
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.file;
+
+/**
+ * 文件名大小限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileSizeLimitExceededException extends FileException
+{
+ private static final long serialVersionUID = 1L;
+
+ public FileSizeLimitExceededException(long defaultMaxSize)
+ {
+ super("upload.exceed.maxSize", new Object[] { defaultMaxSize });
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java
new file mode 100644
index 0000000..f45e7ef
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java
@@ -0,0 +1,61 @@
+package com.ruoyi.common.exception.file;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * 文件上传异常类
+ *
+ * @author ruoyi
+ */
+public class FileUploadException extends Exception
+{
+
+ private static final long serialVersionUID = 1L;
+
+ private final Throwable cause;
+
+ public FileUploadException()
+ {
+ this(null, null);
+ }
+
+ public FileUploadException(final String msg)
+ {
+ this(msg, null);
+ }
+
+ public FileUploadException(String msg, Throwable cause)
+ {
+ super(msg);
+ this.cause = cause;
+ }
+
+ @Override
+ public void printStackTrace(PrintStream stream)
+ {
+ super.printStackTrace(stream);
+ if (cause != null)
+ {
+ stream.println("Caused by:");
+ cause.printStackTrace(stream);
+ }
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter writer)
+ {
+ super.printStackTrace(writer);
+ if (cause != null)
+ {
+ writer.println("Caused by:");
+ cause.printStackTrace(writer);
+ }
+ }
+
+ @Override
+ public Throwable getCause()
+ {
+ return cause;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java
new file mode 100644
index 0000000..011f308
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java
@@ -0,0 +1,80 @@
+package com.ruoyi.common.exception.file;
+
+import java.util.Arrays;
+
+/**
+ * 文件上传 误异常类
+ *
+ * @author ruoyi
+ */
+public class InvalidExtensionException extends FileUploadException
+{
+ private static final long serialVersionUID = 1L;
+
+ private String[] allowedExtension;
+ private String extension;
+ private String filename;
+
+ public InvalidExtensionException(String[] allowedExtension, String extension, String filename)
+ {
+ super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式");
+ this.allowedExtension = allowedExtension;
+ this.extension = extension;
+ this.filename = filename;
+ }
+
+ public String[] getAllowedExtension()
+ {
+ return allowedExtension;
+ }
+
+ public String getExtension()
+ {
+ return extension;
+ }
+
+ public String getFilename()
+ {
+ return filename;
+ }
+
+ public static class InvalidImageExtensionException extends InvalidExtensionException
+ {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename)
+ {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidFlashExtensionException extends InvalidExtensionException
+ {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename)
+ {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidMediaExtensionException extends InvalidExtensionException
+ {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename)
+ {
+ super(allowedExtension, extension, filename);
+ }
+ }
+
+ public static class InvalidVideoExtensionException extends InvalidExtensionException
+ {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename)
+ {
+ super(allowedExtension, extension, filename);
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java
new file mode 100644
index 0000000..a567b40
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java
@@ -0,0 +1,34 @@
+package com.ruoyi.common.exception.job;
+
+/**
+ * 计划策略异常
+ *
+ * @author ruoyi
+ */
+public class TaskException extends Exception
+{
+ private static final long serialVersionUID = 1L;
+
+ private Code code;
+
+ public TaskException(String msg, Code code)
+ {
+ this(msg, code, null);
+ }
+
+ public TaskException(String msg, Code code, Exception nestedEx)
+ {
+ super(msg, nestedEx);
+ this.code = code;
+ }
+
+ public Code getCode()
+ {
+ return code;
+ }
+
+ public enum Code
+ {
+ TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java
new file mode 100644
index 0000000..2bf5038
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 黑名单IP异常类
+ *
+ * @author ruoyi
+ */
+public class BlackListException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public BlackListException()
+ {
+ super("login.blocked", null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java
new file mode 100644
index 0000000..389dbc7
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException()
+ {
+ super("user.jcaptcha.error", null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java
new file mode 100644
index 0000000..85f9486
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 验证码失效异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaExpireException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaExpireException()
+ {
+ super("user.jcaptcha.expire", null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java
new file mode 100644
index 0000000..c292d70
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java
@@ -0,0 +1,18 @@
+package com.ruoyi.common.exception.user;
+
+import com.ruoyi.common.exception.base.BaseException;
+
+/**
+ * 用户信息异常类
+ *
+ * @author ruoyi
+ */
+public class UserException extends BaseException
+{
+ private static final long serialVersionUID = 1L;
+
+ public UserException(String code, Object[] args)
+ {
+ super("user", code, args, null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java
new file mode 100644
index 0000000..eff8181
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 用户不存在异常类
+ *
+ * @author ruoyi
+ */
+public class UserNotExistsException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public UserNotExistsException()
+ {
+ super("user.not.exists", null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java
new file mode 100644
index 0000000..a7f3e5f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 用户密码不正确或不符合规范异常类
+ *
+ * @author ruoyi
+ */
+public class UserPasswordNotMatchException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public UserPasswordNotMatchException()
+ {
+ super("user.password.not.match", null);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java
new file mode 100644
index 0000000..c887cf1
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 用户错误最大次数异常类
+ *
+ * @author ruoyi
+ */
+public class UserPasswordRetryLimitExceedException extends UserException
+{
+ private static final long serialVersionUID = 1L;
+
+ public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime)
+ {
+ super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime });
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java
new file mode 100644
index 0000000..e1e431b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.filter;
+
+import com.alibaba.fastjson2.filter.SimplePropertyPreFilter;
+
+/**
+ * 排除JSON敏感属性
+ *
+ * @author ruoyi
+ */
+public class PropertyPreExcludeFilter extends SimplePropertyPreFilter
+{
+ public PropertyPreExcludeFilter()
+ {
+ }
+
+ public PropertyPreExcludeFilter addExcludes(String... filters)
+ {
+ for (int i = 0; i < filters.length; i++)
+ {
+ this.getExcludes().add(filters[i]);
+ }
+ return this;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java
new file mode 100644
index 0000000..efeded9
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java
@@ -0,0 +1,52 @@
+package com.ruoyi.common.filter;
+
+import java.io.IOException;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.MediaType;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * Repeatable 过滤器
+ *
+ * @author ruoyi
+ */
+public class RepeatableFilter implements Filter
+{
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException
+ {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException
+ {
+ ServletRequest requestWrapper = null;
+ if (request instanceof HttpServletRequest
+ && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
+ {
+ requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
+ }
+ if (null == requestWrapper)
+ {
+ chain.doFilter(request, response);
+ }
+ else
+ {
+ chain.doFilter(requestWrapper, response);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java
new file mode 100644
index 0000000..5c6ee08
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java
@@ -0,0 +1,76 @@
+package com.ruoyi.common.filter;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import com.ruoyi.common.utils.http.HttpHelper;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * 构建可重复读取inputStream的request
+ *
+ * @author ruoyi
+ */
+public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
+{
+ private final byte[] body;
+
+ public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
+ {
+ super(request);
+ request.setCharacterEncoding(Constants.UTF8);
+ response.setCharacterEncoding(Constants.UTF8);
+
+ body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException
+ {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+ @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 int available() throws IOException
+ {
+ return body.length;
+ }
+
+ @Override
+ public boolean isFinished()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+
+ }
+ };
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java
new file mode 100644
index 0000000..7438fc2
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java
@@ -0,0 +1,75 @@
+package com.ruoyi.common.filter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.enums.HttpMethod;
+
+/**
+ * 防止XSS攻击的过滤器
+ *
+ * @author ruoyi
+ */
+public class XssFilter implements Filter
+{
+ /**
+ * 排除链接
+ */
+ public List<String> excludes = new ArrayList<>();
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException
+ {
+ String tempExcludes = filterConfig.getInitParameter("excludes");
+ if (StringUtils.isNotEmpty(tempExcludes))
+ {
+ String[] urls = tempExcludes.split(",");
+ for (String url : urls)
+ {
+ excludes.add(url);
+ }
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException
+ {
+ HttpServletRequest req = (HttpServletRequest) request;
+ HttpServletResponse resp = (HttpServletResponse) response;
+ if (handleExcludeURL(req, resp))
+ {
+ chain.doFilter(request, response);
+ return;
+ }
+ XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
+ chain.doFilter(xssRequest, response);
+ }
+
+ private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response)
+ {
+ String url = request.getServletPath();
+ String method = request.getMethod();
+ // GET DELETE 不过滤
+ if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method))
+ {
+ return true;
+ }
+ return StringUtils.matches(url, excludes);
+ }
+
+ @Override
+ public void destroy()
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java
new file mode 100644
index 0000000..bf7837b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java
@@ -0,0 +1,111 @@
+package com.ruoyi.common.filter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import org.apache.commons.io.IOUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.html.EscapeUtil;
+
+/**
+ * XSS过滤处理
+ *
+ * @author ruoyi
+ */
+public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper
+{
+ /**
+ * @param request
+ */
+ public XssHttpServletRequestWrapper(HttpServletRequest request)
+ {
+ super(request);
+ }
+
+ @Override
+ public String[] getParameterValues(String name)
+ {
+ String[] values = super.getParameterValues(name);
+ if (values != null)
+ {
+ int length = values.length;
+ String[] escapesValues = new String[length];
+ for (int i = 0; i < length; i++)
+ {
+ // 防xss攻击和过滤前后空格
+ escapesValues[i] = EscapeUtil.clean(values[i]).trim();
+ }
+ return escapesValues;
+ }
+ return super.getParameterValues(name);
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException
+ {
+ // 非json类型,直接返回
+ if (!isJsonRequest())
+ {
+ return super.getInputStream();
+ }
+
+ // 为空,直接返回
+ String json = IOUtils.toString(super.getInputStream(), "utf-8");
+ if (StringUtils.isEmpty(json))
+ {
+ return super.getInputStream();
+ }
+
+ // xss过滤
+ json = EscapeUtil.clean(json).trim();
+ byte[] jsonBytes = json.getBytes("utf-8");
+ final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
+ return new ServletInputStream()
+ {
+ @Override
+ public boolean isFinished()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return true;
+ }
+
+ @Override
+ public int available() throws IOException
+ {
+ return jsonBytes.length;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ return bis.read();
+ }
+ };
+ }
+
+ /**
+ * 是否是Json请求
+ *
+ * @param request
+ */
+ public boolean isJsonRequest()
+ {
+ String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
+ return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java
new file mode 100644
index 0000000..b6326c2
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java
@@ -0,0 +1,114 @@
+package com.ruoyi.common.utils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 精确的浮点数运算
+ *
+ * @author ruoyi
+ */
+public class Arith
+{
+
+ /** 默认除法运算精度 */
+ private static final int DEF_DIV_SCALE = 10;
+
+ /** 这个类不能实例化 */
+ private Arith()
+ {
+ }
+
+ /**
+ * 提供精确的加法运算。
+ * @param v1 被加数
+ * @param v2 加数
+ * @return 两个参数的和
+ */
+ public static double add(double v1, double v2)
+ {
+ BigDecimal b1 = new BigDecimal(Double.toString(v1));
+ BigDecimal b2 = new BigDecimal(Double.toString(v2));
+ return b1.add(b2).doubleValue();
+ }
+
+ /**
+ * 提供精确的减法运算。
+ * @param v1 被减数
+ * @param v2 减数
+ * @return 两个参数的差
+ */
+ public static double sub(double v1, double v2)
+ {
+ BigDecimal b1 = new BigDecimal(Double.toString(v1));
+ BigDecimal b2 = new BigDecimal(Double.toString(v2));
+ return b1.subtract(b2).doubleValue();
+ }
+
+ /**
+ * 提供精确的乘法运算。
+ * @param v1 被乘数
+ * @param v2 乘数
+ * @return 两个参数的积
+ */
+ public static double mul(double v1, double v2)
+ {
+ BigDecimal b1 = new BigDecimal(Double.toString(v1));
+ BigDecimal b2 = new BigDecimal(Double.toString(v2));
+ return b1.multiply(b2).doubleValue();
+ }
+
+ /**
+ * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
+ * 小数点以后10位,以后的数字四舍五入。
+ * @param v1 被除数
+ * @param v2 除数
+ * @return 两个参数的商
+ */
+ public static double div(double v1, double v2)
+ {
+ return div(v1, v2, DEF_DIV_SCALE);
+ }
+
+ /**
+ * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
+ * 定精度,以后的数字四舍五入。
+ * @param v1 被除数
+ * @param v2 除数
+ * @param scale 表示表示需要精确到小数点以后几位。
+ * @return 两个参数的商
+ */
+ public static double div(double v1, double v2, int scale)
+ {
+ if (scale < 0)
+ {
+ throw new IllegalArgumentException(
+ "The scale must be a positive integer or zero");
+ }
+ BigDecimal b1 = new BigDecimal(Double.toString(v1));
+ BigDecimal b2 = new BigDecimal(Double.toString(v2));
+ if (b1.compareTo(BigDecimal.ZERO) == 0)
+ {
+ return BigDecimal.ZERO.doubleValue();
+ }
+ return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
+ }
+
+ /**
+ * 提供精确的小数位四舍五入处理。
+ * @param v 需要四舍五入的数字
+ * @param scale 小数点后保留几位
+ * @return 四舍五入后的结果
+ */
+ public static double round(double v, int scale)
+ {
+ if (scale < 0)
+ {
+ throw new IllegalArgumentException(
+ "The scale must be a positive integer or zero");
+ }
+ BigDecimal b = new BigDecimal(Double.toString(v));
+ BigDecimal one = BigDecimal.ONE;
+ return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java
new file mode 100644
index 0000000..fb2ae21
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java
@@ -0,0 +1,191 @@
+package com.ruoyi.common.utils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+/**
+ * 时间工具类
+ *
+ * @author ruoyi
+ */
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils
+{
+ public static String YYYY = "yyyy";
+
+ public static String YYYY_MM = "yyyy-MM";
+
+ public static String YYYY_MM_DD = "yyyy-MM-dd";
+
+ public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+ public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+ private static String[] parsePatterns = {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+ "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+ "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+ /**
+ * 获取当前Date型日期
+ *
+ * @return Date() 当前日期
+ */
+ public static Date getNowDate()
+ {
+ return new Date();
+ }
+
+ /**
+ * 获取当前日期, 默认格式为yyyy-MM-dd
+ *
+ * @return String
+ */
+ public static String getDate()
+ {
+ return dateTimeNow(YYYY_MM_DD);
+ }
+
+ public static final String getTime()
+ {
+ return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+ }
+
+ public static final String dateTimeNow()
+ {
+ return dateTimeNow(YYYYMMDDHHMMSS);
+ }
+
+ public static final String dateTimeNow(final String format)
+ {
+ return parseDateToStr(format, new Date());
+ }
+
+ public static final String dateTime(final Date date)
+ {
+ return parseDateToStr(YYYY_MM_DD, date);
+ }
+
+ public static final String parseDateToStr(final String format, final Date date)
+ {
+ return new SimpleDateFormat(format).format(date);
+ }
+
+ public static final Date dateTime(final String format, final String ts)
+ {
+ try
+ {
+ return new SimpleDateFormat(format).parse(ts);
+ }
+ catch (ParseException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 日期路径 即年/月/日 如2018/08/08
+ */
+ public static final String datePath()
+ {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyy/MM/dd");
+ }
+
+ /**
+ * 日期路径 即年/月/日 如20180808
+ */
+ public static final String dateTime()
+ {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyyMMdd");
+ }
+
+ /**
+ * 日期型字符串转化为日期 格式
+ */
+ public static Date parseDate(Object str)
+ {
+ if (str == null)
+ {
+ return null;
+ }
+ try
+ {
+ return parseDate(str.toString(), parsePatterns);
+ }
+ catch (ParseException e)
+ {
+ return null;
+ }
+ }
+
+ /**
+ * 获取服务器启动时间
+ */
+ public static Date getServerStartDate()
+ {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 计算相差天数
+ */
+ public static int differentDaysByMillisecond(Date date1, Date date2)
+ {
+ return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+ }
+
+ /**
+ * 计算时间差
+ *
+ * @param endDate 最后时间
+ * @param startTime 开始时间
+ * @return 时间差(天/小时/分钟)
+ */
+ public static String timeDistance(Date endDate, Date startTime)
+ {
+ long nd = 1000 * 24 * 60 * 60;
+ long nh = 1000 * 60 * 60;
+ long nm = 1000 * 60;
+ // long ns = 1000;
+ // 获得两个时间的毫秒时间差异
+ long diff = endDate.getTime() - startTime.getTime();
+ // 计算差多少天
+ long day = diff / nd;
+ // 计算差多少小时
+ long hour = diff % nd / nh;
+ // 计算差多少分钟
+ long min = diff % nd % nh / nm;
+ // 计算差多少秒//输出结果
+ // long sec = diff % nd % nh % nm / ns;
+ return day + "天" + hour + "小时" + min + "分钟";
+ }
+
+ /**
+ * 增加 LocalDateTime ==> Date
+ */
+ public static Date toDate(LocalDateTime temporalAccessor)
+ {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 增加 LocalDate ==> Date
+ */
+ public static Date toDate(LocalDate temporalAccessor)
+ {
+ LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+ ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java
new file mode 100644
index 0000000..f8a4c02
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java
@@ -0,0 +1,49 @@
+package com.ruoyi.common.utils;
+
+/**
+ * 脱敏工具类
+ *
+ * @author ruoyi
+ */
+public class DesensitizedUtil
+{
+ /**
+ * 密码的全部字符都用*代替,比如:******
+ *
+ * @param password 密码
+ * @return 脱敏后的密码
+ */
+ public static String password(String password)
+ {
+ if (StringUtils.isBlank(password))
+ {
+ return StringUtils.EMPTY;
+ }
+ return StringUtils.repeat('*', password.length());
+ }
+
+ /**
+ * 车牌中间用*代替,如果是错误的车牌,不处理
+ *
+ * @param carLicense 完整的车牌号
+ * @return 脱敏后的车牌
+ */
+ public static String carLicense(String carLicense)
+ {
+ if (StringUtils.isBlank(carLicense))
+ {
+ return StringUtils.EMPTY;
+ }
+ // 普通车牌
+ if (carLicense.length() == 7)
+ {
+ carLicense = StringUtils.hide(carLicense, 3, 6);
+ }
+ else if (carLicense.length() == 8)
+ {
+ // 新能源车牌
+ carLicense = StringUtils.hide(carLicense, 3, 7);
+ }
+ return carLicense;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java
new file mode 100644
index 0000000..f198462
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java
@@ -0,0 +1,239 @@
+package com.ruoyi.common.utils;
+
+import java.util.Collection;
+import java.util.List;
+import com.alibaba.fastjson2.JSONArray;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.spring.SpringUtils;
+
+/**
+ * 字典工具类
+ *
+ * @author ruoyi
+ */
+public class DictUtils
+{
+ /**
+ * 分隔符
+ */
+ public static final String SEPARATOR = ",";
+
+ /**
+ * 设置字典缓存
+ *
+ * @param key 参数键
+ * @param dictDatas 字典数据列表
+ */
+ public static void setDictCache(String key, List<SysDictData> dictDatas)
+ {
+ SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas);
+ }
+
+ /**
+ * 获取字典缓存
+ *
+ * @param key 参数键
+ * @return dictDatas 字典数据列表
+ */
+ public static List<SysDictData> getDictCache(String key)
+ {
+ JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
+ if (StringUtils.isNotNull(arrayCache))
+ {
+ return arrayCache.toList(SysDictData.class);
+ }
+ return null;
+ }
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @return 字典标签
+ */
+ public static String getDictLabel(String dictType, String dictValue)
+ {
+ if (StringUtils.isEmpty(dictValue))
+ {
+ return StringUtils.EMPTY;
+ }
+ return getDictLabel(dictType, dictValue, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @return 字典值
+ */
+ public static String getDictValue(String dictType, String dictLabel)
+ {
+ if (StringUtils.isEmpty(dictLabel))
+ {
+ return StringUtils.EMPTY;
+ }
+ return getDictValue(dictType, dictLabel, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @param separator 分隔符
+ * @return 字典标签
+ */
+ public static String getDictLabel(String dictType, String dictValue, String separator)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ List<SysDictData> datas = getDictCache(dictType);
+ if (StringUtils.isNull(datas))
+ {
+ return StringUtils.EMPTY;
+ }
+ if (StringUtils.containsAny(separator, dictValue))
+ {
+ for (SysDictData dict : datas)
+ {
+ for (String value : dictValue.split(separator))
+ {
+ if (value.equals(dict.getDictValue()))
+ {
+ propertyString.append(dict.getDictLabel()).append(separator);
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ for (SysDictData dict : datas)
+ {
+ if (dictValue.equals(dict.getDictValue()))
+ {
+ return dict.getDictLabel();
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @param separator 分隔符
+ * @return 字典值
+ */
+ public static String getDictValue(String dictType, String dictLabel, String separator)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ List<SysDictData> datas = getDictCache(dictType);
+ if (StringUtils.isNull(datas))
+ {
+ return StringUtils.EMPTY;
+ }
+ if (StringUtils.containsAny(separator, dictLabel))
+ {
+ for (SysDictData dict : datas)
+ {
+ for (String label : dictLabel.split(separator))
+ {
+ if (label.equals(dict.getDictLabel()))
+ {
+ propertyString.append(dict.getDictValue()).append(separator);
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ for (SysDictData dict : datas)
+ {
+ if (dictLabel.equals(dict.getDictLabel()))
+ {
+ return dict.getDictValue();
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 根据字典类型获取字典所有值
+ *
+ * @param dictType 字典类型
+ * @return 字典值
+ */
+ public static String getDictValues(String dictType)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ List<SysDictData> datas = getDictCache(dictType);
+ if (StringUtils.isNull(datas))
+ {
+ return StringUtils.EMPTY;
+ }
+ for (SysDictData dict : datas)
+ {
+ propertyString.append(dict.getDictValue()).append(SEPARATOR);
+ }
+ return StringUtils.stripEnd(propertyString.toString(), SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型获取字典所有标签
+ *
+ * @param dictType 字典类型
+ * @return 字典值
+ */
+ public static String getDictLabels(String dictType)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ List<SysDictData> datas = getDictCache(dictType);
+ if (StringUtils.isNull(datas))
+ {
+ return StringUtils.EMPTY;
+ }
+ for (SysDictData dict : datas)
+ {
+ propertyString.append(dict.getDictLabel()).append(SEPARATOR);
+ }
+ return StringUtils.stripEnd(propertyString.toString(), SEPARATOR);
+ }
+
+ /**
+ * 删除指定字典缓存
+ *
+ * @param key 字典键
+ */
+ public static void removeDictCache(String key)
+ {
+ SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key));
+ }
+
+ /**
+ * 清空字典缓存
+ */
+ public static void clearDictCache()
+ {
+ Collection<String> keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*");
+ SpringUtils.getBean(RedisCache.class).deleteObject(keys);
+ }
+
+ /**
+ * 设置cache key
+ *
+ * @param configKey 参数键
+ * @return 缓存键key
+ */
+ public static String getCacheKey(String configKey)
+ {
+ return CacheConstants.SYS_DICT_KEY + configKey;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java
new file mode 100644
index 0000000..214e4a0
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java
@@ -0,0 +1,39 @@
+package com.ruoyi.common.utils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+/**
+ * 错误信息处理类。
+ *
+ * @author ruoyi
+ */
+public class ExceptionUtil
+{
+ /**
+ * 获取exception的详细错误信息。
+ */
+ public static String getExceptionMessage(Throwable e)
+ {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw, true));
+ return sw.toString();
+ }
+
+ public static String getRootErrorMessage(Exception e)
+ {
+ Throwable root = ExceptionUtils.getRootCause(e);
+ root = (root == null ? e : root);
+ if (root == null)
+ {
+ return "";
+ }
+ String msg = root.getMessage();
+ if (msg == null)
+ {
+ return "null";
+ }
+ return StringUtils.defaultString(msg);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java
new file mode 100644
index 0000000..0de30c6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java
@@ -0,0 +1,18 @@
+package com.ruoyi.common.utils;
+
+/**
+ * 处理并记录日志文件
+ *
+ * @author ruoyi
+ */
+public class LogUtils
+{
+ public static String getBlock(Object msg)
+ {
+ if (msg == null)
+ {
+ msg = "";
+ }
+ return "[" + msg.toString() + "]";
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java
new file mode 100644
index 0000000..7dac75a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java
@@ -0,0 +1,26 @@
+package com.ruoyi.common.utils;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import com.ruoyi.common.utils.spring.SpringUtils;
+
+/**
+ * 获取i18n资源文件
+ *
+ * @author ruoyi
+ */
+public class MessageUtils
+{
+ /**
+ * 根据消息键和参数 获取消息 委托给spring messageSource
+ *
+ * @param code 消息键
+ * @param args 参数
+ * @return 获取国际化翻译值
+ */
+ public static String message(String code, Object... args)
+ {
+ MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
+ return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java
new file mode 100644
index 0000000..70e9b08
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java
@@ -0,0 +1,35 @@
+package com.ruoyi.common.utils;
+
+import com.github.pagehelper.PageHelper;
+import com.ruoyi.common.core.page.PageDomain;
+import com.ruoyi.common.core.page.TableSupport;
+import com.ruoyi.common.utils.sql.SqlUtil;
+
+/**
+ * 分页工具类
+ *
+ * @author ruoyi
+ */
+public class PageUtils extends PageHelper
+{
+ /**
+ * 设置请求分页数据
+ */
+ public static void startPage()
+ {
+ PageDomain pageDomain = TableSupport.buildPageRequest();
+ Integer pageNum = pageDomain.getPageNum();
+ Integer pageSize = pageDomain.getPageSize();
+ String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+ Boolean reasonable = pageDomain.getReasonable();
+ PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
+ }
+
+ /**
+ * 清理分页的线程变量
+ */
+ public static void clearPage()
+ {
+ PageHelper.clearPage();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java
new file mode 100644
index 0000000..0d3ac5f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java
@@ -0,0 +1,178 @@
+package com.ruoyi.common.utils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.util.PatternMatchUtils;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.exception.ServiceException;
+
+/**
+ * 安全服务工具类
+ *
+ * @author ruoyi
+ */
+public class SecurityUtils
+{
+
+ /**
+ * 用户ID
+ **/
+ public static Long getUserId()
+ {
+ try
+ {
+ return getLoginUser().getUserId();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * 获取部门ID
+ **/
+ public static Long getDeptId()
+ {
+ try
+ {
+ return getLoginUser().getDeptId();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * 获取用户账户
+ **/
+ public static String getUsername()
+ {
+ try
+ {
+ return getLoginUser().getUsername();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * 获取用户
+ **/
+ public static LoginUser getLoginUser()
+ {
+ try
+ {
+ return (LoginUser) getAuthentication().getPrincipal();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * 获取Authentication
+ */
+ public static Authentication getAuthentication()
+ {
+ return SecurityContextHolder.getContext().getAuthentication();
+ }
+
+ /**
+ * 生成BCryptPasswordEncoder密码
+ *
+ * @param password 密码
+ * @return 加密字符串
+ */
+ public static String encryptPassword(String password)
+ {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.encode(password);
+ }
+
+ /**
+ * 判断密码是否相同
+ *
+ * @param rawPassword 真实密码
+ * @param encodedPassword 加密后字符
+ * @return 结果
+ */
+ public static boolean matchesPassword(String rawPassword, String encodedPassword)
+ {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+
+ /**
+ * 是否为管理员
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public static boolean isAdmin(Long userId)
+ {
+ return userId != null && 1L == userId;
+ }
+
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ public static boolean hasPermi(String permission)
+ {
+ return hasPermi(getLoginUser().getPermissions(), permission);
+ }
+
+ /**
+ * 判断是否包含权限
+ *
+ * @param authorities 权限列表
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ public static boolean hasPermi(Collection<String> authorities, String permission)
+ {
+ return authorities.stream().filter(StringUtils::hasText)
+ .anyMatch(x -> Constants.ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission));
+ }
+
+ /**
+ * 验证用户是否拥有某个角色
+ *
+ * @param role 角色标识
+ * @return 用户是否具备某角色
+ */
+ public static boolean hasRole(String role)
+ {
+ List<SysRole> roleList = getLoginUser().getUser().getRoles();
+ Collection<String> roles = roleList.stream().map(SysRole::getRoleKey).collect(Collectors.toSet());
+ return hasRole(roles, role);
+ }
+
+ /**
+ * 判断是否包含角色
+ *
+ * @param roles 角色列表
+ * @param role 角色
+ * @return 用户是否具备某角色权限
+ */
+ public static boolean hasRole(Collection<String> roles, String role)
+ {
+ return roles.stream().filter(StringUtils::hasText)
+ .anyMatch(x -> Constants.SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role));
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java
new file mode 100644
index 0000000..5635db7
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java
@@ -0,0 +1,218 @@
+package com.ruoyi.common.utils;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.text.Convert;
+
+/**
+ * 客户端工具类
+ *
+ * @author ruoyi
+ */
+public class ServletUtils
+{
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name)
+ {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name, String defaultValue)
+ {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name)
+ {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name, Integer defaultValue)
+ {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name)
+ {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name, Boolean defaultValue)
+ {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map<String, String[]> getParams(ServletRequest request)
+ {
+ final Map<String, String[]> map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map<String, String> getParamMap(ServletRequest request)
+ {
+ Map<String, String> params = new HashMap<>();
+ for (Map.Entry<String, String[]> entry : getParams(request).entrySet())
+ {
+ params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
+ }
+ return params;
+ }
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest()
+ {
+ return getRequestAttributes().getRequest();
+ }
+
+ /**
+ * 获取response
+ */
+ public static HttpServletResponse getResponse()
+ {
+ return getRequestAttributes().getResponse();
+ }
+
+ /**
+ * 获取session
+ */
+ public static HttpSession getSession()
+ {
+ return getRequest().getSession();
+ }
+
+ public static ServletRequestAttributes getRequestAttributes()
+ {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ }
+
+ /**
+ * 将字符串渲染到客户端
+ *
+ * @param response 渲染对象
+ * @param string 待渲染的字符串
+ */
+ public static void renderString(HttpServletResponse response, String string)
+ {
+ try
+ {
+ response.setStatus(200);
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+ response.getWriter().print(string);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 是否是Ajax异步请求
+ *
+ * @param request
+ */
+ public static boolean isAjaxRequest(HttpServletRequest request)
+ {
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains("application/json"))
+ {
+ return true;
+ }
+
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest"))
+ {
+ return true;
+ }
+
+ String uri = request.getRequestURI();
+ if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml"))
+ {
+ return true;
+ }
+
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
+ }
+
+ /**
+ * 内容编码
+ *
+ * @param str 内容
+ * @return 编码后的内容
+ */
+ public static String urlEncode(String str)
+ {
+ try
+ {
+ return URLEncoder.encode(str, Constants.UTF8);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ return StringUtils.EMPTY;
+ }
+ }
+
+ /**
+ * 内容解码
+ *
+ * @param str 内容
+ * @return 解码后的内容
+ */
+ public static String urlDecode(String str)
+ {
+ try
+ {
+ return URLDecoder.decode(str, Constants.UTF8);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ return StringUtils.EMPTY;
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java
new file mode 100644
index 0000000..25c2886
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java
@@ -0,0 +1,684 @@
+package com.ruoyi.common.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.springframework.util.AntPathMatcher;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.text.StrFormatter;
+
+/**
+ * 字符串工具类
+ *
+ * @author ruoyi
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils
+{
+ /** 空字符串 */
+ private static final String NULLSTR = "";
+
+ /** 下划线 */
+ private static final char SEPARATOR = '_';
+
+ /** 星号 */
+ private static final char ASTERISK = '*';
+
+ /**
+ * 获取参数不为空值
+ *
+ * @param value defaultValue 要判断的value
+ * @return value 返回值
+ */
+ public static <T> T nvl(T value, T defaultValue)
+ {
+ return value != null ? value : defaultValue;
+ }
+
+ /**
+ * * 判断一个Collection是否为空, 包含List,Set,Queue
+ *
+ * @param coll 要判断的Collection
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty(Collection<?> coll)
+ {
+ return isNull(coll) || coll.isEmpty();
+ }
+
+ /**
+ * * 判断一个Collection是否非空,包含List,Set,Queue
+ *
+ * @param coll 要判断的Collection
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty(Collection<?> coll)
+ {
+ return !isEmpty(coll);
+ }
+
+ /**
+ * * 判断一个对象数组是否为空
+ *
+ * @param objects 要判断的对象数组
+ ** @return true:为空 false:非空
+ */
+ public static boolean isEmpty(Object[] objects)
+ {
+ return isNull(objects) || (objects.length == 0);
+ }
+
+ /**
+ * * 判断一个对象数组是否非空
+ *
+ * @param objects 要判断的对象数组
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty(Object[] objects)
+ {
+ return !isEmpty(objects);
+ }
+
+ /**
+ * * 判断一个Map是否为空
+ *
+ * @param map 要判断的Map
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty(Map<?, ?> map)
+ {
+ return isNull(map) || map.isEmpty();
+ }
+
+ /**
+ * * 判断一个Map是否为空
+ *
+ * @param map 要判断的Map
+ * @return true:非空 false:空
+ */
+ public static boolean isNotEmpty(Map<?, ?> map)
+ {
+ return !isEmpty(map);
+ }
+
+ /**
+ * * 判断一个字符串是否为空串
+ *
+ * @param str String
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty(String str)
+ {
+ return isNull(str) || NULLSTR.equals(str.trim());
+ }
+
+ /**
+ * * 判断一个字符串是否为非空串
+ *
+ * @param str String
+ * @return true:非空串 false:空串
+ */
+ public static boolean isNotEmpty(String str)
+ {
+ return !isEmpty(str);
+ }
+
+ /**
+ * * 判断一个对象是否为空
+ *
+ * @param object Object
+ * @return true:为空 false:非空
+ */
+ public static boolean isNull(Object object)
+ {
+ return object == null;
+ }
+
+ /**
+ * * 判断一个对象是否非空
+ *
+ * @param object Object
+ * @return true:非空 false:空
+ */
+ public static boolean isNotNull(Object object)
+ {
+ return !isNull(object);
+ }
+
+ /**
+ * * 判断一个对象是否是数组类型(Java基本型别的数组)
+ *
+ * @param object 对象
+ * @return true:是数组 false:不是数组
+ */
+ public static boolean isArray(Object object)
+ {
+ return isNotNull(object) && object.getClass().isArray();
+ }
+
+ /**
+ * 去空格
+ */
+ public static String trim(String str)
+ {
+ return (str == null ? "" : str.trim());
+ }
+
+ /**
+ * 替换指定字符串的指定区间内字符为"*"
+ *
+ * @param str 字符串
+ * @param startInclude 开始位置(包含)
+ * @param endExclude 结束位置(不包含)
+ * @return 替换后的字符串
+ */
+ public static String hide(CharSequence str, int startInclude, int endExclude)
+ {
+ if (isEmpty(str))
+ {
+ return NULLSTR;
+ }
+ final int strLength = str.length();
+ if (startInclude > strLength)
+ {
+ return NULLSTR;
+ }
+ if (endExclude > strLength)
+ {
+ endExclude = strLength;
+ }
+ if (startInclude > endExclude)
+ {
+ // 如果起始位置大于结束位置,不替换
+ return NULLSTR;
+ }
+ final char[] chars = new char[strLength];
+ for (int i = 0; i < strLength; i++)
+ {
+ if (i >= startInclude && i < endExclude)
+ {
+ chars[i] = ASTERISK;
+ }
+ else
+ {
+ chars[i] = str.charAt(i);
+ }
+ }
+ return new String(chars);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @return 结果
+ */
+ public static String substring(final String str, int start)
+ {
+ if (str == null)
+ {
+ return NULLSTR;
+ }
+
+ if (start < 0)
+ {
+ start = str.length() + start;
+ }
+
+ if (start < 0)
+ {
+ start = 0;
+ }
+ if (start > str.length())
+ {
+ return NULLSTR;
+ }
+
+ return str.substring(start);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @param end 结束
+ * @return 结果
+ */
+ public static String substring(final String str, int start, int end)
+ {
+ if (str == null)
+ {
+ return NULLSTR;
+ }
+
+ if (end < 0)
+ {
+ end = str.length() + end;
+ }
+ if (start < 0)
+ {
+ start = str.length() + start;
+ }
+
+ if (end > str.length())
+ {
+ end = str.length();
+ }
+
+ if (start > end)
+ {
+ return NULLSTR;
+ }
+
+ if (start < 0)
+ {
+ start = 0;
+ }
+ if (end < 0)
+ {
+ end = 0;
+ }
+
+ return str.substring(start, end);
+ }
+
+ /**
+ * 判断是否为空,并且不是空白字符
+ *
+ * @param str 要判断的value
+ * @return 结果
+ */
+ public static boolean hasText(String str)
+ {
+ return (str != null && !str.isEmpty() && containsText(str));
+ }
+
+ private static boolean containsText(CharSequence str)
+ {
+ int strLen = str.length();
+ for (int i = 0; i < strLen; i++)
+ {
+ if (!Character.isWhitespace(str.charAt(i)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 格式化文本, {} 表示占位符<br>
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+ * 例:<br>
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+ *
+ * @param template 文本模板,被替换的部分用 {} 表示
+ * @param params 参数值
+ * @return 格式化后的文本
+ */
+ public static String format(String template, Object... params)
+ {
+ if (isEmpty(params) || isEmpty(template))
+ {
+ return template;
+ }
+ return StrFormatter.format(template, params);
+ }
+
+ /**
+ * 是否为http(s)://开头
+ *
+ * @param link 链接
+ * @return 结果
+ */
+ public static boolean ishttp(String link)
+ {
+ return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
+ }
+
+ /**
+ * 字符串转set
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @return set集合
+ */
+ public static final Set<String> str2Set(String str, String sep)
+ {
+ return new HashSet<String>(str2List(str, sep, true, false));
+ }
+
+ /**
+ * 字符串转list
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @param filterBlank 过滤纯空白
+ * @param trim 去掉首尾空白
+ * @return list集合
+ */
+ public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
+ {
+ List<String> list = new ArrayList<String>();
+ if (StringUtils.isEmpty(str))
+ {
+ return list;
+ }
+
+ // 过滤空白字符串
+ if (filterBlank && StringUtils.isBlank(str))
+ {
+ return list;
+ }
+ String[] split = str.split(sep);
+ for (String string : split)
+ {
+ if (filterBlank && StringUtils.isBlank(string))
+ {
+ continue;
+ }
+ if (trim)
+ {
+ string = string.trim();
+ }
+ list.add(string);
+ }
+
+ return list;
+ }
+
+ /**
+ * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+ *
+ * @param collection 给定的集合
+ * @param array 给定的数组
+ * @return boolean 结果
+ */
+ public static boolean containsAny(Collection<String> collection, String... array)
+ {
+ if (isEmpty(collection) || isEmpty(array))
+ {
+ return false;
+ }
+ else
+ {
+ for (String str : array)
+ {
+ if (collection.contains(str))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+ *
+ * @param cs 指定字符串
+ * @param searchCharSequences 需要检查的字符串数组
+ * @return 是否包含任意一个字符串
+ */
+ public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
+ {
+ if (isEmpty(cs) || isEmpty(searchCharSequences))
+ {
+ return false;
+ }
+ for (CharSequence testStr : searchCharSequences)
+ {
+ if (containsIgnoreCase(cs, testStr))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 驼峰转下划线命名
+ */
+ public static String toUnderScoreCase(String str)
+ {
+ if (str == null)
+ {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ // 前置字符是否大写
+ boolean preCharIsUpperCase = true;
+ // 当前字符是否大写
+ boolean curreCharIsUpperCase = true;
+ // 下一字符是否大写
+ boolean nexteCharIsUpperCase = true;
+ for (int i = 0; i < str.length(); i++)
+ {
+ char c = str.charAt(i);
+ if (i > 0)
+ {
+ preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+ }
+ else
+ {
+ preCharIsUpperCase = false;
+ }
+
+ curreCharIsUpperCase = Character.isUpperCase(c);
+
+ if (i < (str.length() - 1))
+ {
+ nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+ }
+
+ if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
+ {
+ sb.append(SEPARATOR);
+ }
+ else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
+ {
+ sb.append(SEPARATOR);
+ }
+ sb.append(Character.toLowerCase(c));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * 是否包含字符串
+ *
+ * @param str 验证字符串
+ * @param strs 字符串组
+ * @return 包含返回true
+ */
+ public static boolean inStringIgnoreCase(String str, String... strs)
+ {
+ if (str != null && strs != null)
+ {
+ for (String s : strs)
+ {
+ if (str.equalsIgnoreCase(trim(s)))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+ *
+ * @param name 转换前的下划线大写方式命名的字符串
+ * @return 转换后的驼峰式命名的字符串
+ */
+ public static String convertToCamelCase(String name)
+ {
+ StringBuilder result = new StringBuilder();
+ // 快速检查
+ if (name == null || name.isEmpty())
+ {
+ // 没必要转换
+ return "";
+ }
+ else if (!name.contains("_"))
+ {
+ // 不含下划线,仅将首字母大写
+ return name.substring(0, 1).toUpperCase() + name.substring(1);
+ }
+ // 用下划线将原始字符串分割
+ String[] camels = name.split("_");
+ for (String camel : camels)
+ {
+ // 跳过原始字符串中开头、结尾的下换线或双重下划线
+ if (camel.isEmpty())
+ {
+ continue;
+ }
+ // 首字母大写
+ result.append(camel.substring(0, 1).toUpperCase());
+ result.append(camel.substring(1).toLowerCase());
+ }
+ return result.toString();
+ }
+
+ /**
+ * 驼峰式命名法
+ * 例如:user_name->userName
+ */
+ public static String toCamelCase(String s)
+ {
+ if (s == null)
+ {
+ return null;
+ }
+ if (s.indexOf(SEPARATOR) == -1)
+ {
+ return s;
+ }
+ s = s.toLowerCase();
+ StringBuilder sb = new StringBuilder(s.length());
+ boolean upperCase = false;
+ for (int i = 0; i < s.length(); i++)
+ {
+ char c = s.charAt(i);
+
+ if (c == SEPARATOR)
+ {
+ upperCase = true;
+ }
+ else if (upperCase)
+ {
+ sb.append(Character.toUpperCase(c));
+ upperCase = false;
+ }
+ else
+ {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+ *
+ * @param str 指定字符串
+ * @param strs 需要检查的字符串数组
+ * @return 是否匹配
+ */
+ public static boolean matches(String str, List<String> strs)
+ {
+ if (isEmpty(str) || isEmpty(strs))
+ {
+ return false;
+ }
+ for (String pattern : strs)
+ {
+ if (isMatch(pattern, str))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断url是否与规则配置:
+ * ? 表示单个字符;
+ * * 表示一层路径内的任意字符串,不可跨层级;
+ * ** 表示任意层路径;
+ *
+ * @param pattern 匹配规则
+ * @param url 需要匹配的url
+ * @return
+ */
+ public static boolean isMatch(String pattern, String url)
+ {
+ AntPathMatcher matcher = new AntPathMatcher();
+ return matcher.match(pattern, url);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> T cast(Object obj)
+ {
+ return (T) obj;
+ }
+
+ /**
+ * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+ *
+ * @param num 数字对象
+ * @param size 字符串指定长度
+ * @return 返回数字的字符串格式,该字符串为指定长度。
+ */
+ public static final String padl(final Number num, final int size)
+ {
+ return padl(num.toString(), size, '0');
+ }
+
+ /**
+ * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+ *
+ * @param s 原始字符串
+ * @param size 字符串指定长度
+ * @param c 用于补齐的字符
+ * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+ */
+ public static final String padl(final String s, final int size, final char c)
+ {
+ final StringBuilder sb = new StringBuilder(size);
+ if (s != null)
+ {
+ final int len = s.length();
+ if (s.length() <= size)
+ {
+ for (int i = size - len; i > 0; i--)
+ {
+ sb.append(c);
+ }
+ sb.append(s);
+ }
+ else
+ {
+ return s.substring(len - size, len);
+ }
+ }
+ else
+ {
+ for (int i = size; i > 0; i--)
+ {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java
new file mode 100644
index 0000000..71fe6d5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java
@@ -0,0 +1,99 @@
+package com.ruoyi.common.utils;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 线程相关工具类.
+ *
+ * @author ruoyi
+ */
+public class Threads
+{
+ private static final Logger logger = LoggerFactory.getLogger(Threads.class);
+
+ /**
+ * sleep等待,单位为毫秒
+ */
+ public static void sleep(long milliseconds)
+ {
+ try
+ {
+ Thread.sleep(milliseconds);
+ }
+ catch (InterruptedException e)
+ {
+ return;
+ }
+ }
+
+ /**
+ * 停止线程池
+ * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
+ * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
+ * 如果仍然超時,則強制退出.
+ * 另对在shutdown时线程本身被调用中断做了处理.
+ */
+ public static void shutdownAndAwaitTermination(ExecutorService pool)
+ {
+ if (pool != null && !pool.isShutdown())
+ {
+ pool.shutdown();
+ try
+ {
+ if (!pool.awaitTermination(120, TimeUnit.SECONDS))
+ {
+ pool.shutdownNow();
+ if (!pool.awaitTermination(120, TimeUnit.SECONDS))
+ {
+ logger.info("Pool did not terminate");
+ }
+ }
+ }
+ catch (InterruptedException ie)
+ {
+ pool.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * 打印线程异常信息
+ */
+ public static void printException(Runnable r, Throwable t)
+ {
+ if (t == null && r instanceof Future<?>)
+ {
+ try
+ {
+ Future<?> future = (Future<?>) r;
+ if (future.isDone())
+ {
+ future.get();
+ }
+ }
+ catch (CancellationException ce)
+ {
+ t = ce;
+ }
+ catch (ExecutionException ee)
+ {
+ t = ee.getCause();
+ }
+ catch (InterruptedException ie)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (t != null)
+ {
+ logger.error(t.getMessage(), t);
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java
new file mode 100644
index 0000000..4463662
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java
@@ -0,0 +1,110 @@
+package com.ruoyi.common.utils.bean;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Bean 工具类
+ *
+ * @author ruoyi
+ */
+public class BeanUtils extends org.springframework.beans.BeanUtils
+{
+ /** Bean方法名中属性名开始的下标 */
+ private static final int BEAN_METHOD_PROP_INDEX = 3;
+
+ /** * 匹配getter方法的正则表达式 */
+ private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)");
+
+ /** * 匹配setter方法的正则表达式 */
+ private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)");
+
+ /**
+ * Bean属性复制工具方法。
+ *
+ * @param dest 目标对象
+ * @param src 源对象
+ */
+ public static void copyBeanProp(Object dest, Object src)
+ {
+ try
+ {
+ copyProperties(src, dest);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 获取对象的setter方法。
+ *
+ * @param obj 对象
+ * @return 对象的setter方法列表
+ */
+ public static List<Method> getSetterMethods(Object obj)
+ {
+ // setter方法列表
+ List<Method> setterMethods = new ArrayList<Method>();
+
+ // 获取所有方法
+ Method[] methods = obj.getClass().getMethods();
+
+ // 查找setter方法
+
+ for (Method method : methods)
+ {
+ Matcher m = SET_PATTERN.matcher(method.getName());
+ if (m.matches() && (method.getParameterTypes().length == 1))
+ {
+ setterMethods.add(method);
+ }
+ }
+ // 返回setter方法列表
+ return setterMethods;
+ }
+
+ /**
+ * 获取对象的getter方法。
+ *
+ * @param obj 对象
+ * @return 对象的getter方法列表
+ */
+
+ public static List<Method> getGetterMethods(Object obj)
+ {
+ // getter方法列表
+ List<Method> getterMethods = new ArrayList<Method>();
+ // 获取所有方法
+ Method[] methods = obj.getClass().getMethods();
+ // 查找getter方法
+ for (Method method : methods)
+ {
+ Matcher m = GET_PATTERN.matcher(method.getName());
+ if (m.matches() && (method.getParameterTypes().length == 0))
+ {
+ getterMethods.add(method);
+ }
+ }
+ // 返回getter方法列表
+ return getterMethods;
+ }
+
+ /**
+ * 检查Bean方法名中的属性名是否相等。<br>
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。
+ *
+ * @param m1 方法名1
+ * @param m2 方法名2
+ * @return 属性名一样返回true,否则返回false
+ */
+
+ public static boolean isMethodPropEquals(String m1, String m2)
+ {
+ return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX));
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java
new file mode 100644
index 0000000..898aca5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.utils.bean;
+
+import java.util.Set;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+
+/**
+ * bean对象属性验证
+ *
+ * @author ruoyi
+ */
+public class BeanValidators
+{
+ public static void validateWithException(Validator validator, Object object, Class<?>... groups)
+ throws ConstraintViolationException
+ {
+ Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+ if (!constraintViolations.isEmpty())
+ {
+ throw new ConstraintViolationException(constraintViolations);
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java
new file mode 100644
index 0000000..68130b9
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java
@@ -0,0 +1,76 @@
+package com.ruoyi.common.utils.file;
+
+import java.io.File;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 文件类型工具类
+ *
+ * @author ruoyi
+ */
+public class FileTypeUtils
+{
+ /**
+ * 获取文件类型
+ * <p>
+ * 例如: ruoyi.txt, 返回: txt
+ *
+ * @param file 文件名
+ * @return 后缀(不含".")
+ */
+ public static String getFileType(File file)
+ {
+ if (null == file)
+ {
+ return StringUtils.EMPTY;
+ }
+ return getFileType(file.getName());
+ }
+
+ /**
+ * 获取文件类型
+ * <p>
+ * 例如: ruoyi.txt, 返回: txt
+ *
+ * @param fileName 文件名
+ * @return 后缀(不含".")
+ */
+ public static String getFileType(String fileName)
+ {
+ int separatorIndex = fileName.lastIndexOf(".");
+ if (separatorIndex < 0)
+ {
+ return "";
+ }
+ return fileName.substring(separatorIndex + 1).toLowerCase();
+ }
+
+ /**
+ * 获取文件类型
+ *
+ * @param photoByte 文件字节码
+ * @return 后缀(不含".")
+ */
+ public static String getFileExtendName(byte[] photoByte)
+ {
+ String strFileExtendName = "JPG";
+ if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
+ && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97))
+ {
+ strFileExtendName = "GIF";
+ }
+ else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70))
+ {
+ strFileExtendName = "JPG";
+ }
+ else if ((photoByte[0] == 66) && (photoByte[1] == 77))
+ {
+ strFileExtendName = "BMP";
+ }
+ else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71))
+ {
+ strFileExtendName = "PNG";
+ }
+ return strFileExtendName;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
new file mode 100644
index 0000000..d5455c4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
@@ -0,0 +1,232 @@
+package com.ruoyi.common.utils.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Objects;
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException;
+import com.ruoyi.common.exception.file.FileSizeLimitExceededException;
+import com.ruoyi.common.exception.file.InvalidExtensionException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.uuid.Seq;
+
+/**
+ * 文件上传工具类
+ *
+ * @author ruoyi
+ */
+public class FileUploadUtils
+{
+ /**
+ * 默认大小 50M
+ */
+ public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L;
+
+ /**
+ * 默认的文件名最大长度 100
+ */
+ public static final int DEFAULT_FILE_NAME_LENGTH = 100;
+
+ /**
+ * 默认上传的地址
+ */
+ private static String defaultBaseDir = RuoYiConfig.getProfile();
+
+ public static void setDefaultBaseDir(String defaultBaseDir)
+ {
+ FileUploadUtils.defaultBaseDir = defaultBaseDir;
+ }
+
+ public static String getDefaultBaseDir()
+ {
+ return defaultBaseDir;
+ }
+
+ /**
+ * 以默认配置进行文件上传
+ *
+ * @param file 上传的文件
+ * @return 文件名称
+ * @throws Exception
+ */
+ public static final String upload(MultipartFile file) throws IOException
+ {
+ try
+ {
+ return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
+ }
+ catch (Exception e)
+ {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 根据文件路径上传
+ *
+ * @param baseDir 相对应用的基目录
+ * @param file 上传的文件
+ * @return 文件名称
+ * @throws IOException
+ */
+ public static final String upload(String baseDir, MultipartFile file) throws IOException
+ {
+ try
+ {
+ return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
+ }
+ catch (Exception e)
+ {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 文件上传
+ *
+ * @param baseDir 相对应用的基目录
+ * @param file 上传的文件
+ * @param allowedExtension 上传文件类型
+ * @return 返回上传成功的文件名
+ * @throws FileSizeLimitExceededException 如果超出最大大小
+ * @throws FileNameLengthLimitExceededException 文件名太长
+ * @throws IOException 比如读写文件出错时
+ * @throws InvalidExtensionException 文件校验异常
+ */
+ public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
+ throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
+ InvalidExtensionException
+ {
+ int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
+ if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
+ {
+ throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
+ }
+
+ assertAllowed(file, allowedExtension);
+
+ String fileName = extractFilename(file);
+
+ String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
+ file.transferTo(Paths.get(absPath));
+ return getPathFileName(baseDir, fileName);
+ }
+
+ /**
+ * 编码文件名
+ */
+ public static final String extractFilename(MultipartFile file)
+ {
+ return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
+ FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
+ }
+
+ public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
+ {
+ File desc = new File(uploadDir + File.separator + fileName);
+
+ if (!desc.exists())
+ {
+ if (!desc.getParentFile().exists())
+ {
+ desc.getParentFile().mkdirs();
+ }
+ }
+ return desc;
+ }
+
+ public static final String getPathFileName(String uploadDir, String fileName) throws IOException
+ {
+ int dirLastIndex = RuoYiConfig.getProfile().length() + 1;
+ String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
+ return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
+ }
+
+ /**
+ * 文件大小校验
+ *
+ * @param file 上传的文件
+ * @return
+ * @throws FileSizeLimitExceededException 如果超出最大大小
+ * @throws InvalidExtensionException
+ */
+ public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
+ throws FileSizeLimitExceededException, InvalidExtensionException
+ {
+ long size = file.getSize();
+ if (size > DEFAULT_MAX_SIZE)
+ {
+ throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
+ }
+
+ String fileName = file.getOriginalFilename();
+ String extension = getExtension(file);
+ if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension))
+ {
+ if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION)
+ {
+ throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
+ fileName);
+ }
+ else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION)
+ {
+ throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
+ fileName);
+ }
+ else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION)
+ {
+ throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
+ fileName);
+ }
+ else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION)
+ {
+ throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
+ fileName);
+ }
+ else
+ {
+ throw new InvalidExtensionException(allowedExtension, extension, fileName);
+ }
+ }
+ }
+
+ /**
+ * 判断MIME类型是否是允许的MIME类型
+ *
+ * @param extension
+ * @param allowedExtension
+ * @return
+ */
+ public static final boolean isAllowedExtension(String extension, String[] allowedExtension)
+ {
+ for (String str : allowedExtension)
+ {
+ if (str.equalsIgnoreCase(extension))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 获取文件名的后缀
+ *
+ * @param file 表单文件
+ * @return 后缀名
+ */
+ public static final String getExtension(MultipartFile file)
+ {
+ String extension = FilenameUtils.getExtension(file.getOriginalFilename());
+ if (StringUtils.isEmpty(extension))
+ {
+ extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
+ }
+ return extension;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java
new file mode 100644
index 0000000..2062b75
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java
@@ -0,0 +1,291 @@
+package com.ruoyi.common.utils.file;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import org.apache.commons.io.FilenameUtils;
+
+/**
+ * 文件处理工具类
+ *
+ * @author ruoyi
+ */
+public class FileUtils
+{
+ public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+";
+
+ /**
+ * 输出指定文件的byte数组
+ *
+ * @param filePath 文件路径
+ * @param os 输出流
+ * @return
+ */
+ public static void writeBytes(String filePath, OutputStream os) throws IOException
+ {
+ FileInputStream fis = null;
+ try
+ {
+ File file = new File(filePath);
+ if (!file.exists())
+ {
+ throw new FileNotFoundException(filePath);
+ }
+ fis = new FileInputStream(file);
+ byte[] b = new byte[1024];
+ int length;
+ while ((length = fis.read(b)) > 0)
+ {
+ os.write(b, 0, length);
+ }
+ }
+ catch (IOException e)
+ {
+ throw e;
+ }
+ finally
+ {
+ IOUtils.close(os);
+ IOUtils.close(fis);
+ }
+ }
+
+ /**
+ * 写数据到文件中
+ *
+ * @param data 数据
+ * @return 目标文件
+ * @throws IOException IO异常
+ */
+ public static String writeImportBytes(byte[] data) throws IOException
+ {
+ return writeBytes(data, RuoYiConfig.getImportPath());
+ }
+
+ /**
+ * 写数据到文件中
+ *
+ * @param data 数据
+ * @param uploadDir 目标文件
+ * @return 目标文件
+ * @throws IOException IO异常
+ */
+ public static String writeBytes(byte[] data, String uploadDir) throws IOException
+ {
+ FileOutputStream fos = null;
+ String pathName = "";
+ try
+ {
+ String extension = getFileExtendName(data);
+ pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension;
+ File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName);
+ fos = new FileOutputStream(file);
+ fos.write(data);
+ }
+ finally
+ {
+ IOUtils.close(fos);
+ }
+ return FileUploadUtils.getPathFileName(uploadDir, pathName);
+ }
+
+ /**
+ * 删除文件
+ *
+ * @param filePath 文件
+ * @return
+ */
+ public static boolean deleteFile(String filePath)
+ {
+ boolean flag = false;
+ File file = new File(filePath);
+ // 路径为文件且不为空则进行删除
+ if (file.isFile() && file.exists())
+ {
+ flag = file.delete();
+ }
+ return flag;
+ }
+
+ /**
+ * 文件名称验证
+ *
+ * @param filename 文件名称
+ * @return true 正常 false 非法
+ */
+ public static boolean isValidFilename(String filename)
+ {
+ return filename.matches(FILENAME_PATTERN);
+ }
+
+ /**
+ * 检查文件是否可下载
+ *
+ * @param resource 需要下载的文件
+ * @return true 正常 false 非法
+ */
+ public static boolean checkAllowDownload(String resource)
+ {
+ // 禁止目录上跳级别
+ if (StringUtils.contains(resource, ".."))
+ {
+ return false;
+ }
+
+ // 检查允许下载的文件规则
+ if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource)))
+ {
+ return true;
+ }
+
+ // 不在允许下载的文件规则
+ return false;
+ }
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param request 请求对象
+ * @param fileName 文件名
+ * @return 编码后的文件名
+ */
+ public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException
+ {
+ final String agent = request.getHeader("USER-AGENT");
+ String filename = fileName;
+ if (agent.contains("MSIE"))
+ {
+ // IE浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ filename = filename.replace("+", " ");
+ }
+ else if (agent.contains("Firefox"))
+ {
+ // 火狐浏览器
+ filename = new String(fileName.getBytes(), "ISO8859-1");
+ }
+ else if (agent.contains("Chrome"))
+ {
+ // google浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ }
+ else
+ {
+ // 其它浏览器
+ filename = URLEncoder.encode(filename, "utf-8");
+ }
+ return filename;
+ }
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param response 响应对象
+ * @param realFileName 真实文件名
+ */
+ public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException
+ {
+ String percentEncodedFileName = percentEncode(realFileName);
+
+ StringBuilder contentDispositionValue = new StringBuilder();
+ contentDispositionValue.append("attachment; filename=")
+ .append(percentEncodedFileName)
+ .append(";")
+ .append("filename*=")
+ .append("utf-8''")
+ .append(percentEncodedFileName);
+
+ response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+ response.setHeader("Content-disposition", contentDispositionValue.toString());
+ response.setHeader("download-filename", percentEncodedFileName);
+ }
+
+ /**
+ * 百分号编码工具方法
+ *
+ * @param s 需要百分号编码的字符串
+ * @return 百分号编码后的字符串
+ */
+ public static String percentEncode(String s) throws UnsupportedEncodingException
+ {
+ String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
+ return encode.replaceAll("\\+", "%20");
+ }
+
+ /**
+ * 获取图像后缀
+ *
+ * @param photoByte 图像数据
+ * @return 后缀名
+ */
+ public static String getFileExtendName(byte[] photoByte)
+ {
+ String strFileExtendName = "jpg";
+ if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
+ && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97))
+ {
+ strFileExtendName = "gif";
+ }
+ else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70))
+ {
+ strFileExtendName = "jpg";
+ }
+ else if ((photoByte[0] == 66) && (photoByte[1] == 77))
+ {
+ strFileExtendName = "bmp";
+ }
+ else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71))
+ {
+ strFileExtendName = "png";
+ }
+ return strFileExtendName;
+ }
+
+ /**
+ * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png
+ *
+ * @param fileName 路径名称
+ * @return 没有文件路径的名称
+ */
+ public static String getName(String fileName)
+ {
+ if (fileName == null)
+ {
+ return null;
+ }
+ int lastUnixPos = fileName.lastIndexOf('/');
+ int lastWindowsPos = fileName.lastIndexOf('\\');
+ int index = Math.max(lastUnixPos, lastWindowsPos);
+ return fileName.substring(index + 1);
+ }
+
+ /**
+ * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi
+ *
+ * @param fileName 路径名称
+ * @return 没有文件路径和后缀的名称
+ */
+ public static String getNameNotSuffix(String fileName)
+ {
+ if (fileName == null)
+ {
+ return null;
+ }
+ String baseName = FilenameUtils.getBaseName(fileName);
+ return baseName;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java
new file mode 100644
index 0000000..432dfda
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java
@@ -0,0 +1,98 @@
+package com.ruoyi.common.utils.file;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import org.apache.poi.util.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 图片处理工具类
+ *
+ * @author ruoyi
+ */
+public class ImageUtils
+{
+ private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);
+
+ public static byte[] getImage(String imagePath)
+ {
+ InputStream is = getFile(imagePath);
+ try
+ {
+ return IOUtils.toByteArray(is);
+ }
+ catch (Exception e)
+ {
+ log.error("图片加载异常 {}", e);
+ return null;
+ }
+ finally
+ {
+ IOUtils.closeQuietly(is);
+ }
+ }
+
+ public static InputStream getFile(String imagePath)
+ {
+ try
+ {
+ byte[] result = readFile(imagePath);
+ result = Arrays.copyOf(result, result.length);
+ return new ByteArrayInputStream(result);
+ }
+ catch (Exception e)
+ {
+ log.error("获取图片异常 {}", e);
+ }
+ return null;
+ }
+
+ /**
+ * 读取文件为字节数据
+ *
+ * @param url 地址
+ * @return 字节数据
+ */
+ public static byte[] readFile(String url)
+ {
+ InputStream in = null;
+ try
+ {
+ if (url.startsWith("http"))
+ {
+ // 网络地址
+ URL urlObj = new URL(url);
+ URLConnection urlConnection = urlObj.openConnection();
+ urlConnection.setConnectTimeout(30 * 1000);
+ urlConnection.setReadTimeout(60 * 1000);
+ urlConnection.setDoInput(true);
+ in = urlConnection.getInputStream();
+ }
+ else
+ {
+ // 本机地址
+ String localPath = RuoYiConfig.getProfile();
+ String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX);
+ in = new FileInputStream(downloadPath);
+ }
+ return IOUtils.toByteArray(in);
+ }
+ catch (Exception e)
+ {
+ log.error("获取文件路径异常 {}", e);
+ return null;
+ }
+ finally
+ {
+ IOUtils.closeQuietly(in);
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java
new file mode 100644
index 0000000..f968f1a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java
@@ -0,0 +1,59 @@
+package com.ruoyi.common.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author ruoyi
+ */
+public class MimeTypeUtils
+{
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" };
+
+ public static final String[] FLASH_EXTENSION = { "swf", "flv" };
+
+ public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb" };
+
+ public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" };
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf" };
+
+ public static String getExtension(String prefix)
+ {
+ switch (prefix)
+ {
+ case IMAGE_PNG:
+ return "png";
+ case IMAGE_JPG:
+ return "jpg";
+ case IMAGE_JPEG:
+ return "jpeg";
+ case IMAGE_BMP:
+ return "bmp";
+ case IMAGE_GIF:
+ return "gif";
+ default:
+ return "";
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java
new file mode 100644
index 0000000..f52e83e
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java
@@ -0,0 +1,167 @@
+package com.ruoyi.common.utils.html;
+
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 转义和反转义工具类
+ *
+ * @author ruoyi
+ */
+public class EscapeUtil
+{
+ public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
+
+ private static final char[][] TEXT = new char[64][];
+
+ static
+ {
+ for (int i = 0; i < 64; i++)
+ {
+ TEXT[i] = new char[] { (char) i };
+ }
+
+ // special HTML characters
+ TEXT['\''] = "'".toCharArray(); // 单引号
+ TEXT['"'] = """.toCharArray(); // 双引号
+ TEXT['&'] = "&".toCharArray(); // &符
+ TEXT['<'] = "<".toCharArray(); // 小于号
+ TEXT['>'] = ">".toCharArray(); // 大于号
+ }
+
+ /**
+ * 转义文本中的HTML字符为安全的字符
+ *
+ * @param text 被转义的文本
+ * @return 转义后的文本
+ */
+ public static String escape(String text)
+ {
+ return encode(text);
+ }
+
+ /**
+ * 还原被转义的HTML特殊字符
+ *
+ * @param content 包含转义符的HTML内容
+ * @return 转换后的字符串
+ */
+ public static String unescape(String content)
+ {
+ return decode(content);
+ }
+
+ /**
+ * 清除所有HTML标签,但是不删除标签内的内容
+ *
+ * @param content 文本
+ * @return 清除标签后的文本
+ */
+ public static String clean(String content)
+ {
+ return new HTMLFilter().filter(content);
+ }
+
+ /**
+ * Escape编码
+ *
+ * @param text 被编码的文本
+ * @return 编码后的字符
+ */
+ private static String encode(String text)
+ {
+ if (StringUtils.isEmpty(text))
+ {
+ return StringUtils.EMPTY;
+ }
+
+ final StringBuilder tmp = new StringBuilder(text.length() * 6);
+ char c;
+ for (int i = 0; i < text.length(); i++)
+ {
+ c = text.charAt(i);
+ if (c < 256)
+ {
+ tmp.append("%");
+ if (c < 16)
+ {
+ tmp.append("0");
+ }
+ tmp.append(Integer.toString(c, 16));
+ }
+ else
+ {
+ tmp.append("%u");
+ if (c <= 0xfff)
+ {
+ // issue#I49JU8@Gitee
+ tmp.append("0");
+ }
+ tmp.append(Integer.toString(c, 16));
+ }
+ }
+ return tmp.toString();
+ }
+
+ /**
+ * Escape解码
+ *
+ * @param content 被转义的内容
+ * @return 解码后的字符串
+ */
+ public static String decode(String content)
+ {
+ if (StringUtils.isEmpty(content))
+ {
+ return content;
+ }
+
+ StringBuilder tmp = new StringBuilder(content.length());
+ int lastPos = 0, pos = 0;
+ char ch;
+ while (lastPos < content.length())
+ {
+ pos = content.indexOf("%", lastPos);
+ if (pos == lastPos)
+ {
+ if (content.charAt(pos + 1) == 'u')
+ {
+ ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
+ tmp.append(ch);
+ lastPos = pos + 6;
+ }
+ else
+ {
+ ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
+ tmp.append(ch);
+ lastPos = pos + 3;
+ }
+ }
+ else
+ {
+ if (pos == -1)
+ {
+ tmp.append(content.substring(lastPos));
+ lastPos = content.length();
+ }
+ else
+ {
+ tmp.append(content.substring(lastPos, pos));
+ lastPos = pos;
+ }
+ }
+ }
+ return tmp.toString();
+ }
+
+ public static void main(String[] args)
+ {
+ String html = "<script>alert(1);</script>";
+ String escape = EscapeUtil.escape(html);
+ // String html = "<scr<script>ipt>alert(\"XSS\")</scr<script>ipt>";
+ // String html = "<123";
+ // String html = "123>";
+ System.out.println("clean: " + EscapeUtil.clean(html));
+ System.out.println("escape: " + escape);
+ System.out.println("unescape: " + EscapeUtil.unescape(escape));
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java
new file mode 100644
index 0000000..ebff3fd
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java
@@ -0,0 +1,570 @@
+package com.ruoyi.common.utils.html;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML过滤器,用于去除XSS漏洞隐患。
+ *
+ * @author ruoyi
+ */
+public final class HTMLFilter
+{
+ /**
+ * regex flag union representing /si modifiers in php
+ **/
+ private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
+ private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
+ private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
+ private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
+ private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
+ private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
+ private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
+ private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
+ private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
+ private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
+ private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
+ private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
+ private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
+ private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
+ private static final Pattern P_END_ARROW = Pattern.compile("^>");
+ private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_AMP = Pattern.compile("&");
+ private static final Pattern P_QUOTE = Pattern.compile("\"");
+ private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
+ private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
+ private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
+
+ // @xxx could grow large... maybe use sesat's ReferenceMap
+ private static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>();
+ private static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>();
+
+ /**
+ * set of allowed html elements, along with allowed attributes for each element
+ **/
+ private final Map<String, List<String>> vAllowed;
+ /**
+ * counts of open tags for each (allowable) html element
+ **/
+ private final Map<String, Integer> vTagCounts = new HashMap<>();
+
+ /**
+ * html elements which must always be self-closing (e.g. "<img />")
+ **/
+ private final String[] vSelfClosingTags;
+ /**
+ * html elements which must always have separate opening and closing tags (e.g. "<b></b>")
+ **/
+ private final String[] vNeedClosingTags;
+ /**
+ * set of disallowed html elements
+ **/
+ private final String[] vDisallowed;
+ /**
+ * attributes which should be checked for valid protocols
+ **/
+ private final String[] vProtocolAtts;
+ /**
+ * allowed protocols
+ **/
+ private final String[] vAllowedProtocols;
+ /**
+ * tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />")
+ **/
+ private final String[] vRemoveBlanks;
+ /**
+ * entities allowed within html markup
+ **/
+ private final String[] vAllowedEntities;
+ /**
+ * flag determining whether comments are allowed in input String.
+ */
+ private final boolean stripComment;
+ private final boolean encodeQuotes;
+ /**
+ * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "<b text </b>"
+ * becomes "<b> text </b>"). If set to false, unbalanced angle brackets will be html escaped.
+ */
+ private final boolean alwaysMakeTags;
+
+ /**
+ * Default constructor.
+ */
+ public HTMLFilter()
+ {
+ vAllowed = new HashMap<>();
+
+ final ArrayList<String> a_atts = new ArrayList<>();
+ a_atts.add("href");
+ a_atts.add("target");
+ vAllowed.put("a", a_atts);
+
+ final ArrayList<String> img_atts = new ArrayList<>();
+ img_atts.add("src");
+ img_atts.add("width");
+ img_atts.add("height");
+ img_atts.add("alt");
+ vAllowed.put("img", img_atts);
+
+ final ArrayList<String> no_atts = new ArrayList<>();
+ vAllowed.put("b", no_atts);
+ vAllowed.put("strong", no_atts);
+ vAllowed.put("i", no_atts);
+ vAllowed.put("em", no_atts);
+
+ vSelfClosingTags = new String[] { "img" };
+ vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" };
+ vDisallowed = new String[] {};
+ vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp.
+ vProtocolAtts = new String[] { "src", "href" };
+ vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" };
+ vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" };
+ stripComment = true;
+ encodeQuotes = true;
+ alwaysMakeTags = false;
+ }
+
+ /**
+ * Map-parameter configurable constructor.
+ *
+ * @param conf map containing configuration. keys match field names.
+ */
+ @SuppressWarnings("unchecked")
+ public HTMLFilter(final Map<String, Object> conf)
+ {
+
+ assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
+ assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
+ assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
+ assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
+ assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
+ assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
+ assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
+ assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
+
+ vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
+ vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
+ vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
+ vDisallowed = (String[]) conf.get("vDisallowed");
+ vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
+ vProtocolAtts = (String[]) conf.get("vProtocolAtts");
+ vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
+ vAllowedEntities = (String[]) conf.get("vAllowedEntities");
+ stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
+ encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
+ alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
+ }
+
+ private void reset()
+ {
+ vTagCounts.clear();
+ }
+
+ // ---------------------------------------------------------------
+ // my versions of some PHP library functions
+ public static String chr(final int decimal)
+ {
+ return String.valueOf((char) decimal);
+ }
+
+ public static String htmlSpecialChars(final String s)
+ {
+ String result = s;
+ result = regexReplace(P_AMP, "&", result);
+ result = regexReplace(P_QUOTE, """, result);
+ result = regexReplace(P_LEFT_ARROW, "<", result);
+ result = regexReplace(P_RIGHT_ARROW, ">", result);
+ return result;
+ }
+
+ // ---------------------------------------------------------------
+
+ /**
+ * given a user submitted input String, filter out any invalid or restricted html.
+ *
+ * @param input text (i.e. submitted by a user) than may contain html
+ * @return "clean" version of input, with only valid, whitelisted html elements allowed
+ */
+ public String filter(final String input)
+ {
+ reset();
+ String s = input;
+
+ s = escapeComments(s);
+
+ s = balanceHTML(s);
+
+ s = checkTags(s);
+
+ s = processRemoveBlanks(s);
+
+ // s = validateEntities(s);
+
+ return s;
+ }
+
+ public boolean isAlwaysMakeTags()
+ {
+ return alwaysMakeTags;
+ }
+
+ public boolean isStripComments()
+ {
+ return stripComment;
+ }
+
+ private String escapeComments(final String s)
+ {
+ final Matcher m = P_COMMENTS.matcher(s);
+ final StringBuffer buf = new StringBuffer();
+ if (m.find())
+ {
+ final String match = m.group(1); // (.*?)
+ m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
+ }
+ m.appendTail(buf);
+
+ return buf.toString();
+ }
+
+ private String balanceHTML(String s)
+ {
+ if (alwaysMakeTags)
+ {
+ //
+ // try and form html
+ //
+ s = regexReplace(P_END_ARROW, "", s);
+ // 不追加结束标签
+ s = regexReplace(P_BODY_TO_END, "<$1>", s);
+ s = regexReplace(P_XML_CONTENT, "$1<$2", s);
+
+ }
+ else
+ {
+ //
+ // escape stray brackets
+ //
+ s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s);
+ s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s);
+
+ //
+ // the last regexp causes '<>' entities to appear
+ // (we need to do a lookahead assertion so that the last bracket can
+ // be used in the next pass of the regexp)
+ //
+ s = regexReplace(P_BOTH_ARROWS, "", s);
+ }
+
+ return s;
+ }
+
+ private String checkTags(String s)
+ {
+ Matcher m = P_TAGS.matcher(s);
+
+ final StringBuffer buf = new StringBuffer();
+ while (m.find())
+ {
+ String replaceStr = m.group(1);
+ replaceStr = processTag(replaceStr);
+ m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
+ }
+ m.appendTail(buf);
+
+ // these get tallied in processTag
+ // (remember to reset before subsequent calls to filter method)
+ final StringBuilder sBuilder = new StringBuilder(buf.toString());
+ for (String key : vTagCounts.keySet())
+ {
+ for (int ii = 0; ii < vTagCounts.get(key); ii++)
+ {
+ sBuilder.append("</").append(key).append(">");
+ }
+ }
+ s = sBuilder.toString();
+
+ return s;
+ }
+
+ private String processRemoveBlanks(final String s)
+ {
+ String result = s;
+ for (String tag : vRemoveBlanks)
+ {
+ if (!P_REMOVE_PAIR_BLANKS.containsKey(tag))
+ {
+ P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
+ }
+ result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
+ if (!P_REMOVE_SELF_BLANKS.containsKey(tag))
+ {
+ P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
+ }
+ result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
+ }
+
+ return result;
+ }
+
+ private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s)
+ {
+ Matcher m = regex_pattern.matcher(s);
+ return m.replaceAll(replacement);
+ }
+
+ private String processTag(final String s)
+ {
+ // ending tags
+ Matcher m = P_END_TAG.matcher(s);
+ if (m.find())
+ {
+ final String name = m.group(1).toLowerCase();
+ if (allowed(name))
+ {
+ if (!inArray(name, vSelfClosingTags))
+ {
+ if (vTagCounts.containsKey(name))
+ {
+ vTagCounts.put(name, vTagCounts.get(name) - 1);
+ return "</" + name + ">";
+ }
+ }
+ }
+ }
+
+ // starting tags
+ m = P_START_TAG.matcher(s);
+ if (m.find())
+ {
+ final String name = m.group(1).toLowerCase();
+ final String body = m.group(2);
+ String ending = m.group(3);
+
+ // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
+ if (allowed(name))
+ {
+ final StringBuilder params = new StringBuilder();
+
+ final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
+ final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
+ final List<String> paramNames = new ArrayList<>();
+ final List<String> paramValues = new ArrayList<>();
+ while (m2.find())
+ {
+ paramNames.add(m2.group(1)); // ([a-z0-9]+)
+ paramValues.add(m2.group(3)); // (.*?)
+ }
+ while (m3.find())
+ {
+ paramNames.add(m3.group(1)); // ([a-z0-9]+)
+ paramValues.add(m3.group(3)); // ([^\"\\s']+)
+ }
+
+ String paramName, paramValue;
+ for (int ii = 0; ii < paramNames.size(); ii++)
+ {
+ paramName = paramNames.get(ii).toLowerCase();
+ paramValue = paramValues.get(ii);
+
+ // debug( "paramName='" + paramName + "'" );
+ // debug( "paramValue='" + paramValue + "'" );
+ // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
+
+ if (allowedAttribute(name, paramName))
+ {
+ if (inArray(paramName, vProtocolAtts))
+ {
+ paramValue = processParamProtocol(paramValue);
+ }
+ params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\"");
+ }
+ }
+
+ if (inArray(name, vSelfClosingTags))
+ {
+ ending = " /";
+ }
+
+ if (inArray(name, vNeedClosingTags))
+ {
+ ending = "";
+ }
+
+ if (ending == null || ending.length() < 1)
+ {
+ if (vTagCounts.containsKey(name))
+ {
+ vTagCounts.put(name, vTagCounts.get(name) + 1);
+ }
+ else
+ {
+ vTagCounts.put(name, 1);
+ }
+ }
+ else
+ {
+ ending = " /";
+ }
+ return "<" + name + params + ending + ">";
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ // comments
+ m = P_COMMENT.matcher(s);
+ if (!stripComment && m.find())
+ {
+ return "<" + m.group() + ">";
+ }
+
+ return "";
+ }
+
+ private String processParamProtocol(String s)
+ {
+ s = decodeEntities(s);
+ final Matcher m = P_PROTOCOL.matcher(s);
+ if (m.find())
+ {
+ final String protocol = m.group(1);
+ if (!inArray(protocol, vAllowedProtocols))
+ {
+ // bad protocol, turn into local anchor link instead
+ s = "#" + s.substring(protocol.length() + 1);
+ if (s.startsWith("#//"))
+ {
+ s = "#" + s.substring(3);
+ }
+ }
+ }
+
+ return s;
+ }
+
+ private String decodeEntities(String s)
+ {
+ StringBuffer buf = new StringBuffer();
+
+ Matcher m = P_ENTITY.matcher(s);
+ while (m.find())
+ {
+ final String match = m.group(1);
+ final int decimal = Integer.decode(match).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENTITY_UNICODE.matcher(s);
+ while (m.find())
+ {
+ final String match = m.group(1);
+ final int decimal = Integer.valueOf(match, 16).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENCODE.matcher(s);
+ while (m.find())
+ {
+ final String match = m.group(1);
+ final int decimal = Integer.valueOf(match, 16).intValue();
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ s = validateEntities(s);
+ return s;
+ }
+
+ private String validateEntities(final String s)
+ {
+ StringBuffer buf = new StringBuffer();
+
+ // validate entities throughout the string
+ Matcher m = P_VALID_ENTITIES.matcher(s);
+ while (m.find())
+ {
+ final String one = m.group(1); // ([^&;]*)
+ final String two = m.group(2); // (?=(;|&|$))
+ m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
+ }
+ m.appendTail(buf);
+
+ return encodeQuotes(buf.toString());
+ }
+
+ private String encodeQuotes(final String s)
+ {
+ if (encodeQuotes)
+ {
+ StringBuffer buf = new StringBuffer();
+ Matcher m = P_VALID_QUOTES.matcher(s);
+ while (m.find())
+ {
+ final String one = m.group(1); // (>|^)
+ final String two = m.group(2); // ([^<]+?)
+ final String three = m.group(3); // (<|$)
+ // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two)
+ m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three));
+ }
+ m.appendTail(buf);
+ return buf.toString();
+ }
+ else
+ {
+ return s;
+ }
+ }
+
+ private String checkEntity(final String preamble, final String term)
+ {
+
+ return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble;
+ }
+
+ private boolean isValidEntity(final String entity)
+ {
+ return inArray(entity, vAllowedEntities);
+ }
+
+ private static boolean inArray(final String s, final String[] array)
+ {
+ for (String item : array)
+ {
+ if (item != null && item.equals(s))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean allowed(final String name)
+ {
+ return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
+ }
+
+ private boolean allowedAttribute(final String name, final String paramName)
+ {
+ return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java
new file mode 100644
index 0000000..401f25a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java
@@ -0,0 +1,55 @@
+package com.ruoyi.common.utils.http;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import jakarta.servlet.ServletRequest;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 通用http工具封装
+ *
+ * @author ruoyi
+ */
+public class HttpHelper
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);
+
+ public static String getBodyString(ServletRequest request)
+ {
+ StringBuilder sb = new StringBuilder();
+ BufferedReader reader = null;
+ try (InputStream inputStream = request.getInputStream())
+ {
+ reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ String line = "";
+ while ((line = reader.readLine()) != null)
+ {
+ sb.append(line);
+ }
+ }
+ catch (IOException e)
+ {
+ LOGGER.warn("getBodyString出现问题!");
+ }
+ finally
+ {
+ if (reader != null)
+ {
+ try
+ {
+ reader.close();
+ }
+ catch (IOException e)
+ {
+ LOGGER.error(ExceptionUtils.getMessage(e));
+ }
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
new file mode 100644
index 0000000..ef5eb28
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
@@ -0,0 +1,274 @@
+package com.ruoyi.common.utils.http;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 通用http发送方法
+ *
+ * @author ruoyi
+ */
+public class HttpUtils
+{
+ private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
+
+ /**
+ * 向指定 URL 发送GET方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendGet(String url)
+ {
+ return sendGet(url, StringUtils.EMPTY);
+ }
+
+ /**
+ * 向指定 URL 发送GET方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendGet(String url, String param)
+ {
+ return sendGet(url, param, Constants.UTF8);
+ }
+
+ /**
+ * 向指定 URL 发送GET方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @param contentType 编码类型
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendGet(String url, String param, String contentType)
+ {
+ StringBuilder result = new StringBuilder();
+ BufferedReader in = null;
+ try
+ {
+ String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
+ log.info("sendGet - {}", urlNameString);
+ URL realUrl = new URL(urlNameString);
+ URLConnection connection = realUrl.openConnection();
+ connection.setRequestProperty("accept", "*/*");
+ connection.setRequestProperty("connection", "Keep-Alive");
+ connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+ connection.connect();
+ in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
+ String line;
+ while ((line = in.readLine()) != null)
+ {
+ result.append(line);
+ }
+ log.info("recv - {}", result);
+ }
+ catch (ConnectException e)
+ {
+ log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
+ }
+ catch (SocketTimeoutException e)
+ {
+ log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
+ }
+ catch (IOException e)
+ {
+ log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
+ }
+ catch (Exception e)
+ {
+ log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
+ }
+ finally
+ {
+ try
+ {
+ if (in != null)
+ {
+ in.close();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * 向指定 URL 发送POST方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendPost(String url, String param)
+ {
+ PrintWriter out = null;
+ BufferedReader in = null;
+ StringBuilder result = new StringBuilder();
+ try
+ {
+ log.info("sendPost - {}", url);
+ URL realUrl = new URL(url);
+ URLConnection conn = realUrl.openConnection();
+ conn.setRequestProperty("accept", "*/*");
+ conn.setRequestProperty("connection", "Keep-Alive");
+ conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+ conn.setRequestProperty("Accept-Charset", "utf-8");
+ conn.setRequestProperty("contentType", "utf-8");
+ conn.setDoOutput(true);
+ conn.setDoInput(true);
+ out = new PrintWriter(conn.getOutputStream());
+ out.print(param);
+ out.flush();
+ in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
+ String line;
+ while ((line = in.readLine()) != null)
+ {
+ result.append(line);
+ }
+ log.info("recv - {}", result);
+ }
+ catch (ConnectException e)
+ {
+ log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
+ }
+ catch (SocketTimeoutException e)
+ {
+ log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+ }
+ catch (IOException e)
+ {
+ log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
+ }
+ catch (Exception e)
+ {
+ log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
+ }
+ finally
+ {
+ try
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ if (in != null)
+ {
+ in.close();
+ }
+ }
+ catch (IOException ex)
+ {
+ log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+ }
+ }
+ return result.toString();
+ }
+
+ public static String sendSSLPost(String url, String param)
+ {
+ StringBuilder result = new StringBuilder();
+ String urlNameString = url + "?" + param;
+ try
+ {
+ log.info("sendSSLPost - {}", urlNameString);
+ SSLContext sc = SSLContext.getInstance("SSL");
+ sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
+ URL console = new URL(urlNameString);
+ HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
+ conn.setRequestProperty("accept", "*/*");
+ conn.setRequestProperty("connection", "Keep-Alive");
+ conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+ conn.setRequestProperty("Accept-Charset", "utf-8");
+ conn.setRequestProperty("contentType", "utf-8");
+ conn.setDoOutput(true);
+ conn.setDoInput(true);
+
+ conn.setSSLSocketFactory(sc.getSocketFactory());
+ conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
+ conn.connect();
+ InputStream is = conn.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ String ret = "";
+ while ((ret = br.readLine()) != null)
+ {
+ if (ret != null && !"".equals(ret.trim()))
+ {
+ result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
+ }
+ }
+ log.info("recv - {}", result);
+ conn.disconnect();
+ br.close();
+ }
+ catch (ConnectException e)
+ {
+ log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
+ }
+ catch (SocketTimeoutException e)
+ {
+ log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+ }
+ catch (IOException e)
+ {
+ log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
+ }
+ catch (Exception e)
+ {
+ log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
+ }
+ return result.toString();
+ }
+
+ private static class TrustAnyTrustManager implements X509TrustManager
+ {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ return new X509Certificate[] {};
+ }
+ }
+
+ private static class TrustAnyHostnameVerifier implements HostnameVerifier
+ {
+ @Override
+ public boolean verify(String hostname, SSLSession session)
+ {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java
new file mode 100644
index 0000000..edfe419
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java
@@ -0,0 +1,56 @@
+package com.ruoyi.common.utils.ip;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.http.HttpUtils;
+
+/**
+ * 获取地址类
+ *
+ * @author ruoyi
+ */
+public class AddressUtils
+{
+ private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
+
+ // IP地址查询
+ public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp";
+
+ // 未知地址
+ public static final String UNKNOWN = "XX XX";
+
+ public static String getRealAddressByIP(String ip)
+ {
+ // 内网不查询
+ if (IpUtils.internalIp(ip))
+ {
+ return "内网IP";
+ }
+ if (RuoYiConfig.isAddressEnabled())
+ {
+ try
+ {
+ String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK);
+ if (StringUtils.isEmpty(rspStr))
+ {
+ log.error("获取地理位置异常 {}", ip);
+ return UNKNOWN;
+ }
+ JSONObject obj = JSON.parseObject(rspStr);
+ String region = obj.getString("pro");
+ String city = obj.getString("city");
+ return String.format("%s %s", region, city);
+ }
+ catch (Exception e)
+ {
+ log.error("获取地理位置异常 {}", ip);
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java
new file mode 100644
index 0000000..a376416
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java
@@ -0,0 +1,382 @@
+package com.ruoyi.common.utils.ip;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import jakarta.servlet.http.HttpServletRequest;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 获取IP方法
+ *
+ * @author ruoyi
+ */
+public class IpUtils
+{
+ public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
+ // 匹配 ip
+ public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
+ public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";
+ // 匹配网段
+ public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
+
+ /**
+ * 获取客户端IP
+ *
+ * @return IP地址
+ */
+ public static String getIpAddr()
+ {
+ return getIpAddr(ServletUtils.getRequest());
+ }
+
+ /**
+ * 获取客户端IP
+ *
+ * @param request 请求对象
+ * @return IP地址
+ */
+ public static String getIpAddr(HttpServletRequest request)
+ {
+ if (request == null)
+ {
+ return "unknown";
+ }
+ String ip = request.getHeader("x-forwarded-for");
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
+ {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
+ {
+ ip = request.getHeader("X-Forwarded-For");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
+ {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
+ {
+ ip = request.getHeader("X-Real-IP");
+ }
+
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
+ {
+ ip = request.getRemoteAddr();
+ }
+
+ return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
+ }
+
+ /**
+ * 检查是否为内部IP地址
+ *
+ * @param ip IP地址
+ * @return 结果
+ */
+ public static boolean internalIp(String ip)
+ {
+ byte[] addr = textToNumericFormatV4(ip);
+ return internalIp(addr) || "127.0.0.1".equals(ip);
+ }
+
+ /**
+ * 检查是否为内部IP地址
+ *
+ * @param addr byte地址
+ * @return 结果
+ */
+ private static boolean internalIp(byte[] addr)
+ {
+ if (StringUtils.isNull(addr) || addr.length < 2)
+ {
+ return true;
+ }
+ final byte b0 = addr[0];
+ final byte b1 = addr[1];
+ // 10.x.x.x/8
+ final byte SECTION_1 = 0x0A;
+ // 172.16.x.x/12
+ final byte SECTION_2 = (byte) 0xAC;
+ final byte SECTION_3 = (byte) 0x10;
+ final byte SECTION_4 = (byte) 0x1F;
+ // 192.168.x.x/16
+ final byte SECTION_5 = (byte) 0xC0;
+ final byte SECTION_6 = (byte) 0xA8;
+ switch (b0)
+ {
+ case SECTION_1:
+ return true;
+ case SECTION_2:
+ if (b1 >= SECTION_3 && b1 <= SECTION_4)
+ {
+ return true;
+ }
+ case SECTION_5:
+ switch (b1)
+ {
+ case SECTION_6:
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * 将IPv4地址转换成字节
+ *
+ * @param text IPv4地址
+ * @return byte 字节
+ */
+ public static byte[] textToNumericFormatV4(String text)
+ {
+ if (text.length() == 0)
+ {
+ return null;
+ }
+
+ byte[] bytes = new byte[4];
+ String[] elements = text.split("\\.", -1);
+ try
+ {
+ long l;
+ int i;
+ switch (elements.length)
+ {
+ case 1:
+ l = Long.parseLong(elements[0]);
+ if ((l < 0L) || (l > 4294967295L))
+ {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l >> 24 & 0xFF);
+ bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 2:
+ l = Integer.parseInt(elements[0]);
+ if ((l < 0L) || (l > 255L))
+ {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l & 0xFF);
+ l = Integer.parseInt(elements[1]);
+ if ((l < 0L) || (l > 16777215L))
+ {
+ return null;
+ }
+ bytes[1] = (byte) (int) (l >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 3:
+ for (i = 0; i < 2; ++i)
+ {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L))
+ {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ l = Integer.parseInt(elements[2]);
+ if ((l < 0L) || (l > 65535L))
+ {
+ return null;
+ }
+ bytes[2] = (byte) (int) (l >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 4:
+ for (i = 0; i < 4; ++i)
+ {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L))
+ {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ break;
+ default:
+ return null;
+ }
+ }
+ catch (NumberFormatException e)
+ {
+ return null;
+ }
+ return bytes;
+ }
+
+ /**
+ * 获取IP地址
+ *
+ * @return 本地IP地址
+ */
+ public static String getHostIp()
+ {
+ try
+ {
+ return InetAddress.getLocalHost().getHostAddress();
+ }
+ catch (UnknownHostException e)
+ {
+ }
+ return "127.0.0.1";
+ }
+
+ /**
+ * 获取主机名
+ *
+ * @return 本地主机名
+ */
+ public static String getHostName()
+ {
+ try
+ {
+ return InetAddress.getLocalHost().getHostName();
+ }
+ catch (UnknownHostException e)
+ {
+ }
+ return "未知";
+ }
+
+ /**
+ * 从多级反向代理中获得第一个非unknown IP地址
+ *
+ * @param ip 获得的IP地址
+ * @return 第一个非unknown IP地址
+ */
+ public static String getMultistageReverseProxyIp(String ip)
+ {
+ // 多级反向代理检测
+ if (ip != null && ip.indexOf(",") > 0)
+ {
+ final String[] ips = ip.trim().split(",");
+ for (String subIp : ips)
+ {
+ if (false == isUnknown(subIp))
+ {
+ ip = subIp;
+ break;
+ }
+ }
+ }
+ return StringUtils.substring(ip, 0, 255);
+ }
+
+ /**
+ * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+ *
+ * @param checkString 被检测的字符串
+ * @return 是否未知
+ */
+ public static boolean isUnknown(String checkString)
+ {
+ return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
+ }
+
+ /**
+ * 是否为IP
+ */
+ public static boolean isIP(String ip)
+ {
+ return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
+ }
+
+ /**
+ * 是否为IP,或 *为间隔的通配符地址
+ */
+ public static boolean isIpWildCard(String ip)
+ {
+ return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
+ }
+
+ /**
+ * 检测参数是否在ip通配符里
+ */
+ public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip)
+ {
+ String[] s1 = ipWildCard.split("\\.");
+ String[] s2 = ip.split("\\.");
+ boolean isMatchedSeg = true;
+ for (int i = 0; i < s1.length && !s1[i].equals("*"); i++)
+ {
+ if (!s1[i].equals(s2[i]))
+ {
+ isMatchedSeg = false;
+ break;
+ }
+ }
+ return isMatchedSeg;
+ }
+
+ /**
+ * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
+ */
+ public static boolean isIPSegment(String ipSeg)
+ {
+ return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
+ }
+
+ /**
+ * 判断ip是否在指定网段中
+ */
+ public static boolean ipIsInNetNoCheck(String iparea, String ip)
+ {
+ int idx = iparea.indexOf('-');
+ String[] sips = iparea.substring(0, idx).split("\\.");
+ String[] sipe = iparea.substring(idx + 1).split("\\.");
+ String[] sipt = ip.split("\\.");
+ long ips = 0L, ipe = 0L, ipt = 0L;
+ for (int i = 0; i < 4; ++i)
+ {
+ ips = ips << 8 | Integer.parseInt(sips[i]);
+ ipe = ipe << 8 | Integer.parseInt(sipe[i]);
+ ipt = ipt << 8 | Integer.parseInt(sipt[i]);
+ }
+ if (ips > ipe)
+ {
+ long t = ips;
+ ips = ipe;
+ ipe = t;
+ }
+ return ips <= ipt && ipt <= ipe;
+ }
+
+ /**
+ * 校验ip是否符合过滤串规则
+ *
+ * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
+ * @param ip 校验IP地址
+ * @return boolean 结果
+ */
+ public static boolean isMatchedIp(String filter, String ip)
+ {
+ if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip))
+ {
+ return false;
+ }
+ String[] ips = filter.split(";");
+ for (String iStr : ips)
+ {
+ if (isIP(iStr) && iStr.equals(ip))
+ {
+ return true;
+ }
+ else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip))
+ {
+ return true;
+ }
+ else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java
new file mode 100644
index 0000000..ccab288
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.utils.poi;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Workbook;
+
+/**
+ * Excel数据格式处理适配器
+ *
+ * @author ruoyi
+ */
+public interface ExcelHandlerAdapter
+{
+ /**
+ * 格式化
+ *
+ * @param value 单元格数据值
+ * @param args excel注解args参数组
+ * @param cell 单元格对象
+ * @param wb 工作簿对象
+ *
+ * @return 处理后的值
+ */
+ Object format(Object value, String[] args, Cell cell, Workbook wb);
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
new file mode 100644
index 0000000..68652f2
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -0,0 +1,1812 @@
+package com.ruoyi.common.utils.poi;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
+import org.apache.poi.hssf.usermodel.HSSFPicture;
+import org.apache.poi.hssf.usermodel.HSSFPictureData;
+import org.apache.poi.hssf.usermodel.HSSFShape;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.ClientAnchor;
+import org.apache.poi.ss.usermodel.DataFormat;
+import org.apache.poi.ss.usermodel.DataValidation;
+import org.apache.poi.ss.usermodel.DataValidationConstraint;
+import org.apache.poi.ss.usermodel.DataValidationHelper;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.Drawing;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Name;
+import org.apache.poi.ss.usermodel.PictureData;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.apache.poi.xssf.usermodel.XSSFDrawing;
+import org.apache.poi.xssf.usermodel.XSSFPicture;
+import org.apache.poi.xssf.usermodel.XSSFShape;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.annotation.Excel.Type;
+import com.ruoyi.common.annotation.Excels;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.exception.UtilException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileTypeUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.common.utils.file.ImageUtils;
+import com.ruoyi.common.utils.reflect.ReflectUtils;
+
+/**
+ * Excel相关处理
+ *
+ * @author ruoyi
+ */
+public class ExcelUtil<T>
+{
+ private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
+
+ public static final String FORMULA_REGEX_STR = "=|-|\\+|@";
+
+ public static final String[] FORMULA_STR = { "=", "-", "+", "@" };
+
+ /**
+ * 用于dictType属性数据存储,避免重复查缓存
+ */
+ public Map<String, String> sysDictMap = new HashMap<String, String>();
+
+ /**
+ * Excel sheet最大行数,默认65536
+ */
+ public static final int sheetSize = 65536;
+
+ /**
+ * 工作表名称
+ */
+ private String sheetName;
+
+ /**
+ * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
+ */
+ private Type type;
+
+ /**
+ * 工作薄对象
+ */
+ private Workbook wb;
+
+ /**
+ * 工作表对象
+ */
+ private Sheet sheet;
+
+ /**
+ * 样式列表
+ */
+ private Map<String, CellStyle> styles;
+
+ /**
+ * 导入导出数据列表
+ */
+ private List<T> list;
+
+ /**
+ * 注解列表
+ */
+ private List<Object[]> fields;
+
+ /**
+ * 当前行号
+ */
+ private int rownum;
+
+ /**
+ * 标题
+ */
+ private String title;
+
+ /**
+ * 最大高度
+ */
+ private short maxHeight;
+
+ /**
+ * 合并后最后行数
+ */
+ private int subMergedLastRowNum = 0;
+
+ /**
+ * 合并后开始行数
+ */
+ private int subMergedFirstRowNum = 1;
+
+ /**
+ * 对象的子列表方法
+ */
+ private Method subMethod;
+
+ /**
+ * 对象的子列表属性
+ */
+ private List<Field> subFields;
+
+ /**
+ * 统计列表
+ */
+ private Map<Integer, Double> statistics = new HashMap<Integer, Double>();
+
+ /**
+ * 数字格式
+ */
+ private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
+
+ /**
+ * 实体对象
+ */
+ public Class<T> clazz;
+
+ /**
+ * 需要排除列属性
+ */
+ public String[] excludeFields;
+
+ public ExcelUtil(Class<T> clazz)
+ {
+ this.clazz = clazz;
+ }
+
+ /**
+ * 隐藏Excel中列属性
+ *
+ * @param fields 列属性名 示例[单个"name"/多个"id","name"]
+ * @throws Exception
+ */
+ public void hideColumn(String... fields)
+ {
+ this.excludeFields = fields;
+ }
+
+ public void init(List<T> list, String sheetName, String title, Type type)
+ {
+ if (list == null)
+ {
+ list = new ArrayList<T>();
+ }
+ this.list = list;
+ this.sheetName = sheetName;
+ this.type = type;
+ this.title = title;
+ createExcelField();
+ createWorkbook();
+ createTitle();
+ createSubHead();
+ }
+
+ /**
+ * 创建excel第一行标题
+ */
+ public void createTitle()
+ {
+ if (StringUtils.isNotEmpty(title))
+ {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ int titleLastCol = this.fields.size() - 1;
+ if (isSubList())
+ {
+ titleLastCol = titleLastCol + subFields.size() - 1;
+ }
+ Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
+ titleRow.setHeightInPoints(30);
+ Cell titleCell = titleRow.createCell(0);
+ titleCell.setCellStyle(styles.get("title"));
+ titleCell.setCellValue(title);
+ sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol));
+ }
+ }
+
+ /**
+ * 创建对象的子列表名称
+ */
+ public void createSubHead()
+ {
+ if (isSubList())
+ {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ Row subRow = sheet.createRow(rownum);
+ int excelNum = 0;
+ for (Object[] objects : fields)
+ {
+ Excel attr = (Excel) objects[1];
+ Cell headCell1 = subRow.createCell(excelNum);
+ headCell1.setCellValue(attr.name());
+ headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+ excelNum++;
+ }
+ int headFirstRow = excelNum - 1;
+ int headLastRow = headFirstRow + subFields.size() - 1;
+ if (headLastRow > headFirstRow)
+ {
+ sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow));
+ }
+ rownum++;
+ }
+ }
+
+ /**
+ * 对excel表单默认第一个索引名转换成list
+ *
+ * @param is 输入流
+ * @return 转换后集合
+ */
+ public List<T> importExcel(InputStream is)
+ {
+ List<T> list = null;
+ try
+ {
+ list = importExcel(is, 0);
+ }
+ catch (Exception e)
+ {
+ log.error("导入Excel异常{}", e.getMessage());
+ throw new UtilException(e.getMessage());
+ }
+ finally
+ {
+ IOUtils.closeQuietly(is);
+ }
+ return list;
+ }
+
+ /**
+ * 对excel表单默认第一个索引名转换成list
+ *
+ * @param is 输入流
+ * @param titleNum 标题占用行数
+ * @return 转换后集合
+ */
+ public List<T> importExcel(InputStream is, int titleNum) throws Exception
+ {
+ return importExcel(StringUtils.EMPTY, is, titleNum);
+ }
+
+ /**
+ * 对excel表单指定表格索引名转换成list
+ *
+ * @param sheetName 表格索引名
+ * @param titleNum 标题占用行数
+ * @param is 输入流
+ * @return 转换后集合
+ */
+ public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception
+ {
+ this.type = Type.IMPORT;
+ this.wb = WorkbookFactory.create(is);
+ List<T> list = new ArrayList<T>();
+ // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
+ Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
+ if (sheet == null)
+ {
+ throw new IOException("文件sheet不存在");
+ }
+ boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook);
+ Map<String, PictureData> pictures;
+ if (isXSSFWorkbook)
+ {
+ pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb);
+ }
+ else
+ {
+ pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb);
+ }
+ // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
+ int rows = sheet.getLastRowNum();
+ if (rows > 0)
+ {
+ // 定义一个map用于存放excel列的序号和field.
+ Map<String, Integer> cellMap = new HashMap<String, Integer>();
+ // 获取表头
+ Row heard = sheet.getRow(titleNum);
+ for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
+ {
+ Cell cell = heard.getCell(i);
+ if (StringUtils.isNotNull(cell))
+ {
+ String value = this.getCellValue(heard, i).toString();
+ cellMap.put(value, i);
+ }
+ else
+ {
+ cellMap.put(null, i);
+ }
+ }
+ // 有数据时才处理 得到类的所有field.
+ List<Object[]> fields = this.getFields();
+ Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
+ for (Object[] objects : fields)
+ {
+ Excel attr = (Excel) objects[1];
+ Integer column = cellMap.get(attr.name());
+ if (column != null)
+ {
+ fieldsMap.put(column, objects);
+ }
+ }
+ for (int i = titleNum + 1; i <= rows; i++)
+ {
+ // 从第2行开始取数据,默认第一行是表头.
+ Row row = sheet.getRow(i);
+ // 判断当前行是否是空行
+ if (isRowEmpty(row))
+ {
+ continue;
+ }
+ T entity = null;
+ for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet())
+ {
+ Object val = this.getCellValue(row, entry.getKey());
+
+ // 如果不存在实例则新建.
+ entity = (entity == null ? clazz.getDeclaredConstructor().newInstance() : entity);
+ // 从map中得到对应列的field.
+ Field field = (Field) entry.getValue()[0];
+ Excel attr = (Excel) entry.getValue()[1];
+ // 取得类型,并根据对象类型设置值.
+ Class<?> fieldType = field.getType();
+ if (String.class == fieldType)
+ {
+ String s = Convert.toStr(val);
+ if (StringUtils.endsWith(s, ".0"))
+ {
+ val = StringUtils.substringBefore(s, ".0");
+ }
+ else
+ {
+ String dateFormat = field.getAnnotation(Excel.class).dateFormat();
+ if (StringUtils.isNotEmpty(dateFormat))
+ {
+ val = parseDateToStr(dateFormat, val);
+ }
+ else
+ {
+ val = Convert.toStr(val);
+ }
+ }
+ }
+ else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
+ {
+ val = Convert.toInt(val);
+ }
+ else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
+ {
+ val = Convert.toLong(val);
+ }
+ else if (Double.TYPE == fieldType || Double.class == fieldType)
+ {
+ val = Convert.toDouble(val);
+ }
+ else if (Float.TYPE == fieldType || Float.class == fieldType)
+ {
+ val = Convert.toFloat(val);
+ }
+ else if (BigDecimal.class == fieldType)
+ {
+ val = Convert.toBigDecimal(val);
+ }
+ else if (Date.class == fieldType)
+ {
+ if (val instanceof String)
+ {
+ val = DateUtils.parseDate(val);
+ }
+ else if (val instanceof Double)
+ {
+ val = DateUtil.getJavaDate((Double) val);
+ }
+ }
+ else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
+ {
+ val = Convert.toBool(val, false);
+ }
+ if (StringUtils.isNotNull(fieldType))
+ {
+ String propertyName = field.getName();
+ if (StringUtils.isNotEmpty(attr.targetAttr()))
+ {
+ propertyName = field.getName() + "." + attr.targetAttr();
+ }
+ if (StringUtils.isNotEmpty(attr.readConverterExp()))
+ {
+ val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
+ }
+ else if (StringUtils.isNotEmpty(attr.dictType()))
+ {
+ if (!sysDictMap.containsKey(attr.dictType() + val))
+ {
+ String dictValue = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator());
+ sysDictMap.put(attr.dictType() + val, dictValue);
+ }
+ val = sysDictMap.get(attr.dictType() + val);
+ }
+ else if (!attr.handler().equals(ExcelHandlerAdapter.class))
+ {
+ val = dataFormatHandlerAdapter(val, attr, null);
+ }
+ else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures))
+ {
+ PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey());
+ if (image == null)
+ {
+ val = "";
+ }
+ else
+ {
+ byte[] data = image.getData();
+ val = FileUtils.writeImportBytes(data);
+ }
+ }
+ ReflectUtils.invokeSetter(entity, propertyName, val);
+ }
+ }
+ list.add(entity);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @return 结果
+ */
+ public AjaxResult exportExcel(List<T> list, String sheetName)
+ {
+ return exportExcel(list, sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ * @return 结果
+ */
+ public AjaxResult exportExcel(List<T> list, String sheetName, String title)
+ {
+ this.init(list, sheetName, title, Type.EXPORT);
+ return exportExcel();
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param response 返回数据
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @return 结果
+ */
+ public void exportExcel(HttpServletResponse response, List<T> list, String sheetName)
+ {
+ exportExcel(response, list, sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param response 返回数据
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ * @return 结果
+ */
+ public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title)
+ {
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding("utf-8");
+ this.init(list, sheetName, title, Type.EXPORT);
+ exportExcel(response);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ * @return 结果
+ */
+ public AjaxResult importTemplateExcel(String sheetName)
+ {
+ return importTemplateExcel(sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ * @return 结果
+ */
+ public AjaxResult importTemplateExcel(String sheetName, String title)
+ {
+ this.init(null, sheetName, title, Type.IMPORT);
+ return exportExcel();
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ * @return 结果
+ */
+ public void importTemplateExcel(HttpServletResponse response, String sheetName)
+ {
+ importTemplateExcel(response, sheetName, StringUtils.EMPTY);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @param sheetName 工作表的名称
+ * @param title 标题
+ * @return 结果
+ */
+ public void importTemplateExcel(HttpServletResponse response, String sheetName, String title)
+ {
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding("utf-8");
+ this.init(null, sheetName, title, Type.IMPORT);
+ exportExcel(response);
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @return 结果
+ */
+ public void exportExcel(HttpServletResponse response)
+ {
+ try
+ {
+ writeSheet();
+ wb.write(response.getOutputStream());
+ }
+ catch (Exception e)
+ {
+ log.error("导出Excel异常{}", e.getMessage());
+ }
+ finally
+ {
+ IOUtils.closeQuietly(wb);
+ }
+ }
+
+ /**
+ * 对list数据源将其里面的数据导入到excel表单
+ *
+ * @return 结果
+ */
+ public AjaxResult exportExcel()
+ {
+ OutputStream out = null;
+ try
+ {
+ writeSheet();
+ String filename = encodingFilename(sheetName);
+ out = new FileOutputStream(getAbsoluteFile(filename));
+ wb.write(out);
+ return AjaxResult.success(filename);
+ }
+ catch (Exception e)
+ {
+ log.error("导出Excel异常{}", e.getMessage());
+ throw new UtilException("导出Excel失败,请联系网站管理员!");
+ }
+ finally
+ {
+ IOUtils.closeQuietly(wb);
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * 创建写入数据到Sheet
+ */
+ public void writeSheet()
+ {
+ // 取出一共有多少个sheet.
+ int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
+ for (int index = 0; index < sheetNo; index++)
+ {
+ createSheet(sheetNo, index);
+
+ // 产生一行
+ Row row = sheet.createRow(rownum);
+ int column = 0;
+ // 写入各个字段的列头名称
+ for (Object[] os : fields)
+ {
+ Field field = (Field) os[0];
+ Excel excel = (Excel) os[1];
+ if (Collection.class.isAssignableFrom(field.getType()))
+ {
+ for (Field subField : subFields)
+ {
+ Excel subExcel = subField.getAnnotation(Excel.class);
+ this.createHeadCell(subExcel, row, column++);
+ }
+ }
+ else
+ {
+ this.createHeadCell(excel, row, column++);
+ }
+ }
+ if (Type.EXPORT.equals(type))
+ {
+ fillExcelData(index, row);
+ addStatisticsRow();
+ }
+ }
+ }
+
+ /**
+ * 填充excel数据
+ *
+ * @param index 序号
+ * @param row 单元格行
+ */
+ @SuppressWarnings("unchecked")
+ public void fillExcelData(int index, Row row)
+ {
+ int startNo = index * sheetSize;
+ int endNo = Math.min(startNo + sheetSize, list.size());
+ int rowNo = (1 + rownum) - startNo;
+ for (int i = startNo; i < endNo; i++)
+ {
+ rowNo = isSubList() ? (i > 1 ? rowNo + 1 : rowNo + i) : i + 1 + rownum - startNo;
+ row = sheet.createRow(rowNo);
+ // 得到导出对象.
+ T vo = (T) list.get(i);
+ Collection<?> subList = null;
+ if (isSubList())
+ {
+ if (isSubListValue(vo))
+ {
+ subList = getListCellValue(vo);
+ subMergedLastRowNum = subMergedLastRowNum + subList.size();
+ }
+ else
+ {
+ subMergedFirstRowNum++;
+ subMergedLastRowNum++;
+ }
+ }
+ int column = 0;
+ for (Object[] os : fields)
+ {
+ Field field = (Field) os[0];
+ Excel excel = (Excel) os[1];
+ if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList))
+ {
+ boolean subFirst = false;
+ for (Object obj : subList)
+ {
+ if (subFirst)
+ {
+ rowNo++;
+ row = sheet.createRow(rowNo);
+ }
+ List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
+ int subIndex = 0;
+ for (Field subField : subFields)
+ {
+ if (subField.isAnnotationPresent(Excel.class))
+ {
+ subField.setAccessible(true);
+ Excel attr = subField.getAnnotation(Excel.class);
+ this.addCell(attr, row, (T) obj, subField, column + subIndex);
+ }
+ subIndex++;
+ }
+ subFirst = true;
+ }
+ this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size();
+ }
+ else
+ {
+ this.addCell(excel, row, vo, field, column++);
+ }
+ }
+ }
+ }
+
+ /**
+ * 创建表格样式
+ *
+ * @param wb 工作薄对象
+ * @return 样式列表
+ */
+ private Map<String, CellStyle> createStyles(Workbook wb)
+ {
+ // 写入各条记录,每条记录对应excel表中的一行
+ Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+ CellStyle style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ Font titleFont = wb.createFont();
+ titleFont.setFontName("Arial");
+ titleFont.setFontHeightInPoints((short) 16);
+ titleFont.setBold(true);
+ style.setFont(titleFont);
+ DataFormat dataFormat = wb.createDataFormat();
+ style.setDataFormat(dataFormat.getFormat("@"));
+ styles.put("title", style);
+
+ style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setBorderRight(BorderStyle.THIN);
+ style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderTop(BorderStyle.THIN);
+ style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ Font dataFont = wb.createFont();
+ dataFont.setFontName("Arial");
+ dataFont.setFontHeightInPoints((short) 10);
+ style.setFont(dataFont);
+ styles.put("data", style);
+
+ style = wb.createCellStyle();
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ Font totalFont = wb.createFont();
+ totalFont.setFontName("Arial");
+ totalFont.setFontHeightInPoints((short) 10);
+ style.setFont(totalFont);
+ styles.put("total", style);
+
+ styles.putAll(annotationHeaderStyles(wb, styles));
+
+ styles.putAll(annotationDataStyles(wb));
+
+ return styles;
+ }
+
+ /**
+ * 根据Excel注解创建表格头样式
+ *
+ * @param wb 工作薄对象
+ * @return 自定义样式列表
+ */
+ private Map<String, CellStyle> annotationHeaderStyles(Workbook wb, Map<String, CellStyle> styles)
+ {
+ Map<String, CellStyle> headerStyles = new HashMap<String, CellStyle>();
+ for (Object[] os : fields)
+ {
+ Excel excel = (Excel) os[1];
+ String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor());
+ if (!headerStyles.containsKey(key))
+ {
+ CellStyle style = wb.createCellStyle();
+ style.cloneStyleFrom(styles.get("data"));
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setFillForegroundColor(excel.headerBackgroundColor().index);
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ Font headerFont = wb.createFont();
+ headerFont.setFontName("Arial");
+ headerFont.setFontHeightInPoints((short) 10);
+ headerFont.setBold(true);
+ headerFont.setColor(excel.headerColor().index);
+ style.setFont(headerFont);
+ // 设置表格头单元格文本形式
+ DataFormat dataFormat = wb.createDataFormat();
+ style.setDataFormat(dataFormat.getFormat("@"));
+ headerStyles.put(key, style);
+ }
+ }
+ return headerStyles;
+ }
+
+ /**
+ * 根据Excel注解创建表格列样式
+ *
+ * @param wb 工作薄对象
+ * @return 自定义样式列表
+ */
+ private Map<String, CellStyle> annotationDataStyles(Workbook wb)
+ {
+ Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+ for (Object[] os : fields)
+ {
+ Field field = (Field) os[0];
+ Excel excel = (Excel) os[1];
+ if (Collection.class.isAssignableFrom(field.getType()))
+ {
+ ParameterizedType pt = (ParameterizedType) field.getGenericType();
+ Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
+ List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+ for (Field subField : subFields)
+ {
+ Excel subExcel = subField.getAnnotation(Excel.class);
+ annotationDataStyles(styles, subField, subExcel);
+ }
+ }
+ else
+ {
+ annotationDataStyles(styles, field, excel);
+ }
+ }
+ return styles;
+ }
+
+ /**
+ * 根据Excel注解创建表格列样式
+ *
+ * @param styles 自定义样式列表
+ * @param field 属性列信息
+ * @param excel 注解信息
+ */
+ public void annotationDataStyles(Map<String, CellStyle> styles, Field field, Excel excel)
+ {
+ String key = StringUtils.format("data_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType());
+ if (!styles.containsKey(key))
+ {
+ CellStyle style = wb.createCellStyle();
+ style.setAlignment(excel.align());
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setBorderRight(BorderStyle.THIN);
+ style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderTop(BorderStyle.THIN);
+ style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ style.setFillForegroundColor(excel.backgroundColor().getIndex());
+ Font dataFont = wb.createFont();
+ dataFont.setFontName("Arial");
+ dataFont.setFontHeightInPoints((short) 10);
+ dataFont.setColor(excel.color().index);
+ style.setFont(dataFont);
+ if (ColumnType.TEXT == excel.cellType())
+ {
+ DataFormat dataFormat = wb.createDataFormat();
+ style.setDataFormat(dataFormat.getFormat("@"));
+ }
+ styles.put(key, style);
+ }
+ }
+
+ /**
+ * 创建单元格
+ */
+ public Cell createHeadCell(Excel attr, Row row, int column)
+ {
+ // 创建列
+ Cell cell = row.createCell(column);
+ // 写入列信息
+ cell.setCellValue(attr.name());
+ setDataValidation(attr, row, column);
+ cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+ if (isSubList())
+ {
+ // 填充默认样式,防止合并单元格样式失效
+ sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType())));
+ if (attr.needMerge())
+ {
+ sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column));
+ }
+ }
+ return cell;
+ }
+
+ /**
+ * 设置单元格信息
+ *
+ * @param value 单元格值
+ * @param attr 注解相关
+ * @param cell 单元格信息
+ */
+ public void setCellVo(Object value, Excel attr, Cell cell)
+ {
+ if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType())
+ {
+ String cellValue = Convert.toStr(value);
+ // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。
+ if (StringUtils.startsWithAny(cellValue, FORMULA_STR))
+ {
+ cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0");
+ }
+ if (value instanceof Collection && StringUtils.equals("[]", cellValue))
+ {
+ cellValue = StringUtils.EMPTY;
+ }
+ cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix());
+ }
+ else if (ColumnType.NUMERIC == attr.cellType())
+ {
+ if (StringUtils.isNotNull(value))
+ {
+ cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
+ }
+ }
+ else if (ColumnType.IMAGE == attr.cellType())
+ {
+ ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1);
+ String imagePath = Convert.toStr(value);
+ if (StringUtils.isNotEmpty(imagePath))
+ {
+ byte[] data = ImageUtils.getImage(imagePath);
+ getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
+ cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
+ }
+ }
+ }
+
+ /**
+ * 获取画布
+ */
+ public static Drawing<?> getDrawingPatriarch(Sheet sheet)
+ {
+ if (sheet.getDrawingPatriarch() == null)
+ {
+ sheet.createDrawingPatriarch();
+ }
+ return sheet.getDrawingPatriarch();
+ }
+
+ /**
+ * 获取图片类型,设置图片插入类型
+ */
+ public int getImageType(byte[] value)
+ {
+ String type = FileTypeUtils.getFileExtendName(value);
+ if ("JPG".equalsIgnoreCase(type))
+ {
+ return Workbook.PICTURE_TYPE_JPEG;
+ }
+ else if ("PNG".equalsIgnoreCase(type))
+ {
+ return Workbook.PICTURE_TYPE_PNG;
+ }
+ return Workbook.PICTURE_TYPE_JPEG;
+ }
+
+ /**
+ * 创建表格样式
+ */
+ public void setDataValidation(Excel attr, Row row, int column)
+ {
+ if (attr.name().indexOf("注:") >= 0)
+ {
+ sheet.setColumnWidth(column, 6000);
+ }
+ else
+ {
+ // 设置列宽
+ sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
+ }
+ if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0 || attr.comboReadDict())
+ {
+ String[] comboArray = attr.combo();
+ if (attr.comboReadDict())
+ {
+ if (!sysDictMap.containsKey("combo_" + attr.dictType()))
+ {
+ String labels = DictUtils.getDictLabels(attr.dictType());
+ sysDictMap.put("combo_" + attr.dictType(), labels);
+ }
+ String val = sysDictMap.get("combo_" + attr.dictType());
+ comboArray = StringUtils.split(val, DictUtils.SEPARATOR);
+ }
+ if (comboArray.length > 15 || StringUtils.join(comboArray).length() > 255)
+ {
+ // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到
+ setXSSFValidationWithHidden(sheet, comboArray, attr.prompt(), 1, 100, column, column);
+ }
+ else
+ {
+ // 提示信息或只能选择不能输入的列内容.
+ setPromptOrValidation(sheet, comboArray, attr.prompt(), 1, 100, column, column);
+ }
+ }
+ }
+
+ /**
+ * 添加单元格
+ */
+ public Cell addCell(Excel attr, Row row, T vo, Field field, int column)
+ {
+ Cell cell = null;
+ try
+ {
+ // 设置行高
+ row.setHeight(maxHeight);
+ // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
+ if (attr.isExport())
+ {
+ // 创建cell
+ cell = row.createCell(column);
+ if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge())
+ {
+ CellRangeAddress cellAddress = new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column);
+ sheet.addMergedRegion(cellAddress);
+ }
+ cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType())));
+
+ // 用于读取对象中的属性
+ Object value = getTargetValue(vo, field, attr);
+ String dateFormat = attr.dateFormat();
+ String readConverterExp = attr.readConverterExp();
+ String separator = attr.separator();
+ String dictType = attr.dictType();
+ if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value))
+ {
+ cell.setCellValue(parseDateToStr(dateFormat, value));
+ }
+ else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value))
+ {
+ cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
+ }
+ else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value))
+ {
+ if (!sysDictMap.containsKey(dictType + value))
+ {
+ String lable = convertDictByExp(Convert.toStr(value), dictType, separator);
+ sysDictMap.put(dictType + value, lable);
+ }
+ cell.setCellValue(sysDictMap.get(dictType + value));
+ }
+ else if (value instanceof BigDecimal && -1 != attr.scale())
+ {
+ cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue());
+ }
+ else if (!attr.handler().equals(ExcelHandlerAdapter.class))
+ {
+ cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell));
+ }
+ else
+ {
+ // 设置列类型
+ setCellVo(value, attr, cell);
+ }
+ addStatisticsData(column, Convert.toStr(value), attr);
+ }
+ }
+ catch (Exception e)
+ {
+ log.error("导出Excel失败{}", e);
+ }
+ return cell;
+ }
+
+ /**
+ * 设置 POI XSSFSheet 单元格提示或选择框
+ *
+ * @param sheet 表单
+ * @param textlist 下拉框显示的内容
+ * @param promptContent 提示内容
+ * @param firstRow 开始行
+ * @param endRow 结束行
+ * @param firstCol 开始列
+ * @param endCol 结束列
+ */
+ public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow,
+ int firstCol, int endCol)
+ {
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1");
+ CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+ DataValidation dataValidation = helper.createValidation(constraint, regions);
+ if (StringUtils.isNotEmpty(promptContent))
+ {
+ // 如果设置了提示信息则鼠标放上去提示
+ dataValidation.createPromptBox("", promptContent);
+ dataValidation.setShowPromptBox(true);
+ }
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation)
+ {
+ dataValidation.setSuppressDropDownArrow(true);
+ dataValidation.setShowErrorBox(true);
+ }
+ else
+ {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+ sheet.addValidationData(dataValidation);
+ }
+
+ /**
+ * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框).
+ *
+ * @param sheet 要设置的sheet.
+ * @param textlist 下拉框显示的内容
+ * @param promptContent 提示内容
+ * @param firstRow 开始行
+ * @param endRow 结束行
+ * @param firstCol 开始列
+ * @param endCol 结束列
+ */
+ public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol)
+ {
+ String hideSheetName = "combo_" + firstCol + "_" + endCol;
+ Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据
+ for (int i = 0; i < textlist.length; i++)
+ {
+ hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]);
+ }
+ // 创建名称,可被其他单元格引用
+ Name name = wb.createName();
+ name.setNameName(hideSheetName + "_data");
+ name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length);
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ // 加载下拉列表内容
+ DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data");
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+ // 数据有效性对象
+ DataValidation dataValidation = helper.createValidation(constraint, regions);
+ if (StringUtils.isNotEmpty(promptContent))
+ {
+ // 如果设置了提示信息则鼠标放上去提示
+ dataValidation.createPromptBox("", promptContent);
+ dataValidation.setShowPromptBox(true);
+ }
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation)
+ {
+ dataValidation.setSuppressDropDownArrow(true);
+ dataValidation.setShowErrorBox(true);
+ }
+ else
+ {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+
+ sheet.addValidationData(dataValidation);
+ // 设置hiddenSheet隐藏
+ wb.setSheetHidden(wb.getSheetIndex(hideSheet), true);
+ }
+
+ /**
+ * 解析导出值 0=男,1=女,2=未知
+ *
+ * @param propertyValue 参数值
+ * @param converterExp 翻译注解
+ * @param separator 分隔符
+ * @return 解析后值
+ */
+ public static String convertByExp(String propertyValue, String converterExp, String separator)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(",");
+ for (String item : convertSource)
+ {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator))
+ {
+ for (String value : propertyValue.split(separator))
+ {
+ if (itemArray[0].equals(value))
+ {
+ propertyString.append(itemArray[1] + separator);
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (itemArray[0].equals(propertyValue))
+ {
+ return itemArray[1];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 反向解析值 男=0,女=1,未知=2
+ *
+ * @param propertyValue 参数值
+ * @param converterExp 翻译注解
+ * @param separator 分隔符
+ * @return 解析后值
+ */
+ public static String reverseByExp(String propertyValue, String converterExp, String separator)
+ {
+ StringBuilder propertyString = new StringBuilder();
+ String[] convertSource = converterExp.split(",");
+ for (String item : convertSource)
+ {
+ String[] itemArray = item.split("=");
+ if (StringUtils.containsAny(propertyValue, separator))
+ {
+ for (String value : propertyValue.split(separator))
+ {
+ if (itemArray[1].equals(value))
+ {
+ propertyString.append(itemArray[0] + separator);
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (itemArray[1].equals(propertyValue))
+ {
+ return itemArray[0];
+ }
+ }
+ }
+ return StringUtils.stripEnd(propertyString.toString(), separator);
+ }
+
+ /**
+ * 解析字典值
+ *
+ * @param dictValue 字典值
+ * @param dictType 字典类型
+ * @param separator 分隔符
+ * @return 字典标签
+ */
+ public static String convertDictByExp(String dictValue, String dictType, String separator)
+ {
+ return DictUtils.getDictLabel(dictType, dictValue, separator);
+ }
+
+ /**
+ * 反向解析值字典值
+ *
+ * @param dictLabel 字典标签
+ * @param dictType 字典类型
+ * @param separator 分隔符
+ * @return 字典值
+ */
+ public static String reverseDictByExp(String dictLabel, String dictType, String separator)
+ {
+ return DictUtils.getDictValue(dictType, dictLabel, separator);
+ }
+
+ /**
+ * 数据处理器
+ *
+ * @param value 数据值
+ * @param excel 数据注解
+ * @return
+ */
+ public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell)
+ {
+ try
+ {
+ Object instance = excel.handler().getDeclaredConstructor().newInstance();
+ Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class });
+ value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb);
+ }
+ catch (Exception e)
+ {
+ log.error("不能格式化数据 " + excel.handler(), e.getMessage());
+ }
+ return Convert.toStr(value);
+ }
+
+ /**
+ * 合计统计信息
+ */
+ private void addStatisticsData(Integer index, String text, Excel entity)
+ {
+ if (entity != null && entity.isStatistics())
+ {
+ Double temp = 0D;
+ if (!statistics.containsKey(index))
+ {
+ statistics.put(index, temp);
+ }
+ try
+ {
+ temp = Double.valueOf(text);
+ }
+ catch (NumberFormatException e)
+ {
+ }
+ statistics.put(index, statistics.get(index) + temp);
+ }
+ }
+
+ /**
+ * 创建统计行
+ */
+ public void addStatisticsRow()
+ {
+ if (statistics.size() > 0)
+ {
+ Row row = sheet.createRow(sheet.getLastRowNum() + 1);
+ Set<Integer> keys = statistics.keySet();
+ Cell cell = row.createCell(0);
+ cell.setCellStyle(styles.get("total"));
+ cell.setCellValue("合计");
+
+ for (Integer key : keys)
+ {
+ cell = row.createCell(key);
+ cell.setCellStyle(styles.get("total"));
+ cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
+ }
+ statistics.clear();
+ }
+ }
+
+ /**
+ * 编码文件名
+ */
+ public String encodingFilename(String filename)
+ {
+ filename = UUID.randomUUID() + "_" + filename + ".xlsx";
+ return filename;
+ }
+
+ /**
+ * 获取下载路径
+ *
+ * @param filename 文件名称
+ */
+ public String getAbsoluteFile(String filename)
+ {
+ String downloadPath = RuoYiConfig.getDownloadPath() + filename;
+ File desc = new File(downloadPath);
+ if (!desc.getParentFile().exists())
+ {
+ desc.getParentFile().mkdirs();
+ }
+ return downloadPath;
+ }
+
+ /**
+ * 获取bean中的属性值
+ *
+ * @param vo 实体对象
+ * @param field 字段
+ * @param excel 注解
+ * @return 最终的属性值
+ * @throws Exception
+ */
+ private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
+ {
+ Object o = field.get(vo);
+ if (StringUtils.isNotEmpty(excel.targetAttr()))
+ {
+ String target = excel.targetAttr();
+ if (target.contains("."))
+ {
+ String[] targets = target.split("[.]");
+ for (String name : targets)
+ {
+ o = getValue(o, name);
+ }
+ }
+ else
+ {
+ o = getValue(o, target);
+ }
+ }
+ return o;
+ }
+
+ /**
+ * 以类的属性的get方法方法形式获取值
+ *
+ * @param o
+ * @param name
+ * @return value
+ * @throws Exception
+ */
+ private Object getValue(Object o, String name) throws Exception
+ {
+ if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name))
+ {
+ Class<?> clazz = o.getClass();
+ Field field = clazz.getDeclaredField(name);
+ field.setAccessible(true);
+ o = field.get(o);
+ }
+ return o;
+ }
+
+ /**
+ * 得到所有定义字段
+ */
+ private void createExcelField()
+ {
+ this.fields = getFields();
+ this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
+ this.maxHeight = getRowHeight();
+ }
+
+ /**
+ * 获取字段注解信息
+ */
+ public List<Object[]> getFields()
+ {
+ List<Object[]> fields = new ArrayList<Object[]>();
+ List<Field> tempFields = new ArrayList<>();
+ tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
+ tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ for (Field field : tempFields)
+ {
+ if (!ArrayUtils.contains(this.excludeFields, field.getName()))
+ {
+ // 单注解
+ if (field.isAnnotationPresent(Excel.class))
+ {
+ Excel attr = field.getAnnotation(Excel.class);
+ if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
+ {
+ field.setAccessible(true);
+ fields.add(new Object[] { field, attr });
+ }
+ if (Collection.class.isAssignableFrom(field.getType()))
+ {
+ subMethod = getSubMethod(field.getName(), clazz);
+ ParameterizedType pt = (ParameterizedType) field.getGenericType();
+ Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
+ this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+ }
+ }
+
+ // 多注解
+ if (field.isAnnotationPresent(Excels.class))
+ {
+ Excels attrs = field.getAnnotation(Excels.class);
+ Excel[] excels = attrs.value();
+ for (Excel attr : excels)
+ {
+ if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr())
+ && (attr != null && (attr.type() == Type.ALL || attr.type() == type)))
+ {
+ field.setAccessible(true);
+ fields.add(new Object[] { field, attr });
+ }
+ }
+ }
+ }
+ }
+ return fields;
+ }
+
+ /**
+ * 根据注解获取最大行高
+ */
+ public short getRowHeight()
+ {
+ double maxHeight = 0;
+ for (Object[] os : this.fields)
+ {
+ Excel excel = (Excel) os[1];
+ maxHeight = Math.max(maxHeight, excel.height());
+ }
+ return (short) (maxHeight * 20);
+ }
+
+ /**
+ * 创建一个工作簿
+ */
+ public void createWorkbook()
+ {
+ this.wb = new SXSSFWorkbook(500);
+ this.sheet = wb.createSheet();
+ wb.setSheetName(0, sheetName);
+ this.styles = createStyles(wb);
+ }
+
+ /**
+ * 创建工作表
+ *
+ * @param sheetNo sheet数量
+ * @param index 序号
+ */
+ public void createSheet(int sheetNo, int index)
+ {
+ // 设置工作表的名称.
+ if (sheetNo > 1 && index > 0)
+ {
+ this.sheet = wb.createSheet();
+ this.createTitle();
+ wb.setSheetName(index, sheetName + index);
+ }
+ }
+
+ /**
+ * 获取单元格值
+ *
+ * @param row 获取的行
+ * @param column 获取单元格列号
+ * @return 单元格值
+ */
+ public Object getCellValue(Row row, int column)
+ {
+ if (row == null)
+ {
+ return row;
+ }
+ Object val = "";
+ try
+ {
+ Cell cell = row.getCell(column);
+ if (StringUtils.isNotNull(cell))
+ {
+ if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA)
+ {
+ val = cell.getNumericCellValue();
+ if (DateUtil.isCellDateFormatted(cell))
+ {
+ val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
+ }
+ else
+ {
+ if ((Double) val % 1 != 0)
+ {
+ val = new BigDecimal(val.toString());
+ }
+ else
+ {
+ val = new DecimalFormat("0").format(val);
+ }
+ }
+ }
+ else if (cell.getCellType() == CellType.STRING)
+ {
+ val = cell.getStringCellValue();
+ }
+ else if (cell.getCellType() == CellType.BOOLEAN)
+ {
+ val = cell.getBooleanCellValue();
+ }
+ else if (cell.getCellType() == CellType.ERROR)
+ {
+ val = cell.getErrorCellValue();
+ }
+
+ }
+ }
+ catch (Exception e)
+ {
+ return val;
+ }
+ return val;
+ }
+
+ /**
+ * 判断是否是空行
+ *
+ * @param row 判断的行
+ * @return
+ */
+ private boolean isRowEmpty(Row row)
+ {
+ if (row == null)
+ {
+ return true;
+ }
+ for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++)
+ {
+ Cell cell = row.getCell(i);
+ if (cell != null && cell.getCellType() != CellType.BLANK)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 获取Excel2003图片
+ *
+ * @param sheet 当前sheet对象
+ * @param workbook 工作簿对象
+ * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData
+ */
+ public static Map<String, PictureData> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook)
+ {
+ Map<String, PictureData> sheetIndexPicMap = new HashMap<String, PictureData>();
+ List<HSSFPictureData> pictures = workbook.getAllPictures();
+ if (!pictures.isEmpty())
+ {
+ for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren())
+ {
+ HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor();
+ if (shape instanceof HSSFPicture)
+ {
+ HSSFPicture pic = (HSSFPicture) shape;
+ int pictureIndex = pic.getPictureIndex() - 1;
+ HSSFPictureData picData = pictures.get(pictureIndex);
+ String picIndex = anchor.getRow1() + "_" + anchor.getCol1();
+ sheetIndexPicMap.put(picIndex, picData);
+ }
+ }
+ return sheetIndexPicMap;
+ }
+ else
+ {
+ return sheetIndexPicMap;
+ }
+ }
+
+ /**
+ * 获取Excel2007图片
+ *
+ * @param sheet 当前sheet对象
+ * @param workbook 工作簿对象
+ * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData
+ */
+ public static Map<String, PictureData> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook)
+ {
+ Map<String, PictureData> sheetIndexPicMap = new HashMap<String, PictureData>();
+ for (POIXMLDocumentPart dr : sheet.getRelations())
+ {
+ if (dr instanceof XSSFDrawing)
+ {
+ XSSFDrawing drawing = (XSSFDrawing) dr;
+ List<XSSFShape> shapes = drawing.getShapes();
+ for (XSSFShape shape : shapes)
+ {
+ if (shape instanceof XSSFPicture)
+ {
+ XSSFPicture pic = (XSSFPicture) shape;
+ XSSFClientAnchor anchor = pic.getPreferredSize();
+ CTMarker ctMarker = anchor.getFrom();
+ String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol();
+ sheetIndexPicMap.put(picIndex, pic.getPictureData());
+ }
+ }
+ }
+ }
+ return sheetIndexPicMap;
+ }
+
+ /**
+ * 格式化不同类型的日期对象
+ *
+ * @param dateFormat 日期格式
+ * @param val 被格式化的日期对象
+ * @return 格式化后的日期字符
+ */
+ public String parseDateToStr(String dateFormat, Object val)
+ {
+ if (val == null)
+ {
+ return "";
+ }
+ String str;
+ if (val instanceof Date)
+ {
+ str = DateUtils.parseDateToStr(dateFormat, (Date) val);
+ }
+ else if (val instanceof LocalDateTime)
+ {
+ str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val));
+ }
+ else if (val instanceof LocalDate)
+ {
+ str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val));
+ }
+ else
+ {
+ str = val.toString();
+ }
+ return str;
+ }
+
+ /**
+ * 是否有对象的子列表
+ */
+ public boolean isSubList()
+ {
+ return StringUtils.isNotNull(subFields) && subFields.size() > 0;
+ }
+
+ /**
+ * 是否有对象的子列表,集合不为空
+ */
+ public boolean isSubListValue(T vo)
+ {
+ return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0;
+ }
+
+ /**
+ * 获取集合的值
+ */
+ public Collection<?> getListCellValue(Object obj)
+ {
+ Object value;
+ try
+ {
+ value = subMethod.invoke(obj, new Object[] {});
+ }
+ catch (Exception e)
+ {
+ return new ArrayList<Object>();
+ }
+ return (Collection<?>) value;
+ }
+
+ /**
+ * 获取对象的子列表方法
+ *
+ * @param name 名称
+ * @param pojoClass 类对象
+ * @return 子列表方法
+ */
+ public Method getSubMethod(String name, Class<?> pojoClass)
+ {
+ StringBuffer getMethodName = new StringBuffer("get");
+ getMethodName.append(name.substring(0, 1).toUpperCase());
+ getMethodName.append(name.substring(1));
+ Method method = null;
+ try
+ {
+ method = pojoClass.getMethod(getMethodName.toString(), new Class[] {});
+ }
+ catch (Exception e)
+ {
+ log.error("获取对象异常{}", e.getMessage());
+ }
+ return method;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java
new file mode 100644
index 0000000..b19953e
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java
@@ -0,0 +1,410 @@
+package com.ruoyi.common.utils.reflect;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Date;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.utils.DateUtils;
+
+/**
+ * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
+ *
+ * @author ruoyi
+ */
+@SuppressWarnings("rawtypes")
+public class ReflectUtils
+{
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ private static final String CGLIB_CLASS_SEPARATOR = "$$";
+
+ private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class);
+
+ /**
+ * 调用Getter方法.
+ * 支持多级,如:对象名.对象名.方法
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E invokeGetter(Object obj, String propertyName)
+ {
+ Object object = obj;
+ for (String name : StringUtils.split(propertyName, "."))
+ {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {});
+ }
+ return (E) object;
+ }
+
+ /**
+ * 调用Setter方法, 仅匹配方法名。
+ * 支持多级,如:对象名.对象名.方法
+ */
+ public static <E> void invokeSetter(Object obj, String propertyName, E value)
+ {
+ Object object = obj;
+ String[] names = StringUtils.split(propertyName, ".");
+ for (int i = 0; i < names.length; i++)
+ {
+ if (i < names.length - 1)
+ {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+ object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {});
+ }
+ else
+ {
+ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+ invokeMethodByName(object, setterMethodName, new Object[] { value });
+ }
+ }
+ }
+
+ /**
+ * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E getFieldValue(final Object obj, final String fieldName)
+ {
+ Field field = getAccessibleField(obj, fieldName);
+ if (field == null)
+ {
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ return null;
+ }
+ E result = null;
+ try
+ {
+ result = (E) field.get(obj);
+ }
+ catch (IllegalAccessException e)
+ {
+ logger.error("不可能抛出的异常{}", e.getMessage());
+ }
+ return result;
+ }
+
+ /**
+ * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数.
+ */
+ public static <E> void setFieldValue(final Object obj, final String fieldName, final E value)
+ {
+ Field field = getAccessibleField(obj, fieldName);
+ if (field == null)
+ {
+ // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+ return;
+ }
+ try
+ {
+ field.set(obj, value);
+ }
+ catch (IllegalAccessException e)
+ {
+ logger.error("不可能抛出的异常: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 直接调用对象方法, 无视private/protected修饰符.
+ * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用.
+ * 同时匹配方法名+参数类型,
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E invokeMethod(final Object obj, final String methodName, final Class<?>[] parameterTypes,
+ final Object[] args)
+ {
+ if (obj == null || methodName == null)
+ {
+ return null;
+ }
+ Method method = getAccessibleMethod(obj, methodName, parameterTypes);
+ if (method == null)
+ {
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+ return null;
+ }
+ try
+ {
+ return (E) method.invoke(obj, args);
+ }
+ catch (Exception e)
+ {
+ String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+ throw convertReflectionExceptionToUnchecked(msg, e);
+ }
+ }
+
+ /**
+ * 直接调用对象方法, 无视private/protected修饰符,
+ * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用.
+ * 只匹配函数名,如果有多个同名函数调用第一个。
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E invokeMethodByName(final Object obj, final String methodName, final Object[] args)
+ {
+ Method method = getAccessibleMethodByName(obj, methodName, args.length);
+ if (method == null)
+ {
+ // 如果为空不报错,直接返回空。
+ logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+ return null;
+ }
+ try
+ {
+ // 类型转换(将参数数据类型转换为目标方法参数类型)
+ Class<?>[] cs = method.getParameterTypes();
+ for (int i = 0; i < cs.length; i++)
+ {
+ if (args[i] != null && !args[i].getClass().equals(cs[i]))
+ {
+ if (cs[i] == String.class)
+ {
+ args[i] = Convert.toStr(args[i]);
+ if (StringUtils.endsWith((String) args[i], ".0"))
+ {
+ args[i] = StringUtils.substringBefore((String) args[i], ".0");
+ }
+ }
+ else if (cs[i] == Integer.class)
+ {
+ args[i] = Convert.toInt(args[i]);
+ }
+ else if (cs[i] == Long.class)
+ {
+ args[i] = Convert.toLong(args[i]);
+ }
+ else if (cs[i] == Double.class)
+ {
+ args[i] = Convert.toDouble(args[i]);
+ }
+ else if (cs[i] == Float.class)
+ {
+ args[i] = Convert.toFloat(args[i]);
+ }
+ else if (cs[i] == Date.class)
+ {
+ if (args[i] instanceof String)
+ {
+ args[i] = DateUtils.parseDate(args[i]);
+ }
+ else
+ {
+ args[i] = DateUtil.getJavaDate((Double) args[i]);
+ }
+ }
+ else if (cs[i] == boolean.class || cs[i] == Boolean.class)
+ {
+ args[i] = Convert.toBool(args[i]);
+ }
+ }
+ }
+ return (E) method.invoke(obj, args);
+ }
+ catch (Exception e)
+ {
+ String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+ throw convertReflectionExceptionToUnchecked(msg, e);
+ }
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ */
+ public static Field getAccessibleField(final Object obj, final String fieldName)
+ {
+ // 为空不报错。直接返回 null
+ if (obj == null)
+ {
+ return null;
+ }
+ Validate.notBlank(fieldName, "fieldName can't be blank");
+ for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass())
+ {
+ try
+ {
+ Field field = superClass.getDeclaredField(fieldName);
+ makeAccessible(field);
+ return field;
+ }
+ catch (NoSuchFieldException e)
+ {
+ continue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ * 匹配函数名+参数类型。
+ * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+ */
+ public static Method getAccessibleMethod(final Object obj, final String methodName,
+ final Class<?>... parameterTypes)
+ {
+ // 为空不报错。直接返回 null
+ if (obj == null)
+ {
+ return null;
+ }
+ Validate.notBlank(methodName, "methodName can't be blank");
+ for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass())
+ {
+ try
+ {
+ Method method = searchType.getDeclaredMethod(methodName, parameterTypes);
+ makeAccessible(method);
+ return method;
+ }
+ catch (NoSuchMethodException e)
+ {
+ continue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+ * 如向上转型到Object仍无法找到, 返回null.
+ * 只匹配函数名。
+ * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+ */
+ public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum)
+ {
+ // 为空不报错。直接返回 null
+ if (obj == null)
+ {
+ return null;
+ }
+ Validate.notBlank(methodName, "methodName can't be blank");
+ for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass())
+ {
+ Method[] methods = searchType.getDeclaredMethods();
+ for (Method method : methods)
+ {
+ if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum)
+ {
+ makeAccessible(method);
+ return method;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+ */
+ public static void makeAccessible(Method method)
+ {
+ if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
+ && !method.isAccessible())
+ {
+ method.setAccessible(true);
+ }
+ }
+
+ /**
+ * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+ */
+ public static void makeAccessible(Field field)
+ {
+ if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
+ || Modifier.isFinal(field.getModifiers())) && !field.isAccessible())
+ {
+ field.setAccessible(true);
+ }
+ }
+
+ /**
+ * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处
+ * 如无法找到, 返回Object.class.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Class<T> getClassGenricType(final Class clazz)
+ {
+ return getClassGenricType(clazz, 0);
+ }
+
+ /**
+ * 通过反射, 获得Class定义中声明的父类的泛型参数的类型.
+ * 如无法找到, 返回Object.class.
+ */
+ public static Class getClassGenricType(final Class clazz, final int index)
+ {
+ Type genType = clazz.getGenericSuperclass();
+
+ if (!(genType instanceof ParameterizedType))
+ {
+ logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType");
+ return Object.class;
+ }
+
+ Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
+
+ if (index >= params.length || index < 0)
+ {
+ logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
+ + params.length);
+ return Object.class;
+ }
+ if (!(params[index] instanceof Class))
+ {
+ logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
+ return Object.class;
+ }
+
+ return (Class) params[index];
+ }
+
+ public static Class<?> getUserClass(Object instance)
+ {
+ if (instance == null)
+ {
+ throw new RuntimeException("Instance must not be null");
+ }
+ Class clazz = instance.getClass();
+ if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR))
+ {
+ Class<?> superClass = clazz.getSuperclass();
+ if (superClass != null && !Object.class.equals(superClass))
+ {
+ return superClass;
+ }
+ }
+ return clazz;
+
+ }
+
+ /**
+ * 将反射时的checked exception转换为unchecked exception.
+ */
+ public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e)
+ {
+ if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
+ || e instanceof NoSuchMethodException)
+ {
+ return new IllegalArgumentException(msg, e);
+ }
+ else if (e instanceof InvocationTargetException)
+ {
+ return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException());
+ }
+ return new RuntimeException(msg, e);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java
new file mode 100644
index 0000000..ca1cd92
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java
@@ -0,0 +1,291 @@
+package com.ruoyi.common.utils.sign;
+
+/**
+ * Base64工具类
+ *
+ * @author ruoyi
+ */
+public final class Base64
+{
+ static private final int BASELENGTH = 128;
+ static private final int LOOKUPLENGTH = 64;
+ static private final int TWENTYFOURBITGROUP = 24;
+ static private final int EIGHTBIT = 8;
+ static private final int SIXTEENBIT = 16;
+ static private final int FOURBYTE = 4;
+ static private final int SIGN = -128;
+ static private final char PAD = '=';
+ static final private byte[] base64Alphabet = new byte[BASELENGTH];
+ static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+ static
+ {
+ for (int i = 0; i < BASELENGTH; ++i)
+ {
+ base64Alphabet[i] = -1;
+ }
+ for (int i = 'Z'; i >= 'A'; i--)
+ {
+ base64Alphabet[i] = (byte) (i - 'A');
+ }
+ for (int i = 'z'; i >= 'a'; i--)
+ {
+ base64Alphabet[i] = (byte) (i - 'a' + 26);
+ }
+
+ for (int i = '9'; i >= '0'; i--)
+ {
+ base64Alphabet[i] = (byte) (i - '0' + 52);
+ }
+
+ base64Alphabet['+'] = 62;
+ base64Alphabet['/'] = 63;
+
+ for (int i = 0; i <= 25; i++)
+ {
+ lookUpBase64Alphabet[i] = (char) ('A' + i);
+ }
+
+ for (int i = 26, j = 0; i <= 51; i++, j++)
+ {
+ lookUpBase64Alphabet[i] = (char) ('a' + j);
+ }
+
+ for (int i = 52, j = 0; i <= 61; i++, j++)
+ {
+ lookUpBase64Alphabet[i] = (char) ('0' + j);
+ }
+ lookUpBase64Alphabet[62] = (char) '+';
+ lookUpBase64Alphabet[63] = (char) '/';
+ }
+
+ private static boolean isWhiteSpace(char octect)
+ {
+ return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+ }
+
+ private static boolean isPad(char octect)
+ {
+ return (octect == PAD);
+ }
+
+ private static boolean isData(char octect)
+ {
+ return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+ }
+
+ /**
+ * Encodes hex octects into Base64
+ *
+ * @param binaryData Array containing binaryData
+ * @return Encoded Base64 array
+ */
+ public static String encode(byte[] binaryData)
+ {
+ if (binaryData == null)
+ {
+ return null;
+ }
+
+ int lengthDataBits = binaryData.length * EIGHTBIT;
+ if (lengthDataBits == 0)
+ {
+ return "";
+ }
+
+ int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+ int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+ int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
+ char encodedData[] = null;
+
+ encodedData = new char[numberQuartet * 4];
+
+ byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+ int encodedIndex = 0;
+ int dataIndex = 0;
+
+ for (int i = 0; i < numberTriplets; i++)
+ {
+ b1 = binaryData[dataIndex++];
+ b2 = binaryData[dataIndex++];
+ b3 = binaryData[dataIndex++];
+
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+ byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+ }
+
+ // form integral number of 6-bit groups
+ if (fewerThan24bits == EIGHTBIT)
+ {
+ b1 = binaryData[dataIndex];
+ k = (byte) (b1 & 0x03);
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+ encodedData[encodedIndex++] = PAD;
+ encodedData[encodedIndex++] = PAD;
+ }
+ else if (fewerThan24bits == SIXTEENBIT)
+ {
+ b1 = binaryData[dataIndex];
+ b2 = binaryData[dataIndex + 1];
+ l = (byte) (b2 & 0x0f);
+ k = (byte) (b1 & 0x03);
+
+ byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+ byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+ encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+ encodedData[encodedIndex++] = PAD;
+ }
+ return new String(encodedData);
+ }
+
+ /**
+ * Decodes Base64 data into octects
+ *
+ * @param encoded string containing Base64 data
+ * @return Array containind decoded data.
+ */
+ public static byte[] decode(String encoded)
+ {
+ if (encoded == null)
+ {
+ return null;
+ }
+
+ char[] base64Data = encoded.toCharArray();
+ // remove white spaces
+ int len = removeWhiteSpace(base64Data);
+
+ if (len % FOURBYTE != 0)
+ {
+ return null;// should be divisible by four
+ }
+
+ int numberQuadruple = (len / FOURBYTE);
+
+ if (numberQuadruple == 0)
+ {
+ return new byte[0];
+ }
+
+ byte decodedData[] = null;
+ byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+ char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+ int i = 0;
+ int encodedIndex = 0;
+ int dataIndex = 0;
+ decodedData = new byte[(numberQuadruple) * 3];
+
+ for (; i < numberQuadruple - 1; i++)
+ {
+
+ if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
+ || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++])))
+ {
+ return null;
+ } // if found "no data" just return null
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+ }
+
+ if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])))
+ {
+ return null;// if found "no data" just return null
+ }
+
+ b1 = base64Alphabet[d1];
+ b2 = base64Alphabet[d2];
+
+ d3 = base64Data[dataIndex++];
+ d4 = base64Data[dataIndex++];
+ if (!isData((d3)) || !isData((d4)))
+ {// Check if they are PAD characters
+ if (isPad(d3) && isPad(d4))
+ {
+ if ((b2 & 0xf) != 0)// last 4 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 1];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+ return tmp;
+ }
+ else if (!isPad(d3) && isPad(d4))
+ {
+ b3 = base64Alphabet[d3];
+ if ((b3 & 0x3) != 0)// last 2 bits should be zero
+ {
+ return null;
+ }
+ byte[] tmp = new byte[i * 3 + 2];
+ System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+ tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ return tmp;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ { // No PAD e.g 3cQl
+ b3 = base64Alphabet[d3];
+ b4 = base64Alphabet[d4];
+ decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+ decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+ decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+ }
+ return decodedData;
+ }
+
+ /**
+ * remove WhiteSpace from MIME containing encoded Base64 data.
+ *
+ * @param data the byte array of base64 data (with WS)
+ * @return the new length
+ */
+ private static int removeWhiteSpace(char[] data)
+ {
+ if (data == null)
+ {
+ return 0;
+ }
+
+ // count characters that's not whitespace
+ int newSize = 0;
+ int len = data.length;
+ for (int i = 0; i < len; i++)
+ {
+ if (!isWhiteSpace(data[i]))
+ {
+ data[newSize++] = data[i];
+ }
+ }
+ return newSize;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java
new file mode 100644
index 0000000..c1c58db
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java
@@ -0,0 +1,67 @@
+package com.ruoyi.common.utils.sign;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Md5加密方法
+ *
+ * @author ruoyi
+ */
+public class Md5Utils
+{
+ private static final Logger log = LoggerFactory.getLogger(Md5Utils.class);
+
+ private static byte[] md5(String s)
+ {
+ MessageDigest algorithm;
+ try
+ {
+ algorithm = MessageDigest.getInstance("MD5");
+ algorithm.reset();
+ algorithm.update(s.getBytes("UTF-8"));
+ byte[] messageDigest = algorithm.digest();
+ return messageDigest;
+ }
+ catch (Exception e)
+ {
+ log.error("MD5 Error...", e);
+ }
+ return null;
+ }
+
+ private static final String toHex(byte hash[])
+ {
+ if (hash == null)
+ {
+ return null;
+ }
+ StringBuffer buf = new StringBuffer(hash.length * 2);
+ int i;
+
+ for (i = 0; i < hash.length; i++)
+ {
+ if ((hash[i] & 0xff) < 0x10)
+ {
+ buf.append("0");
+ }
+ buf.append(Long.toString(hash[i] & 0xff, 16));
+ }
+ return buf.toString();
+ }
+
+ public static String hash(String s)
+ {
+ try
+ {
+ return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+ }
+ catch (Exception e)
+ {
+ log.error("not supported charset...{}", e);
+ return s;
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java
new file mode 100644
index 0000000..f290ec3
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java
@@ -0,0 +1,158 @@
+package com.ruoyi.common.utils.spring;
+
+import org.springframework.aop.framework.AopContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * spring工具类 方便在非spring管理环境中获取bean
+ *
+ * @author ruoyi
+ */
+@Component
+public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
+{
+ /** Spring应用上下文环境 */
+ private static ConfigurableListableBeanFactory beanFactory;
+
+ private static ApplicationContext applicationContext;
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
+ {
+ SpringUtils.beanFactory = beanFactory;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
+ {
+ SpringUtils.applicationContext = applicationContext;
+ }
+
+ /**
+ * 获取对象
+ *
+ * @param name
+ * @return Object 一个以所给名字注册的bean的实例
+ * @throws org.springframework.beans.BeansException
+ *
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getBean(String name) throws BeansException
+ {
+ return (T) beanFactory.getBean(name);
+ }
+
+ /**
+ * 获取类型为requiredType的对象
+ *
+ * @param clz
+ * @return
+ * @throws org.springframework.beans.BeansException
+ *
+ */
+ public static <T> T getBean(Class<T> clz) throws BeansException
+ {
+ T result = (T) beanFactory.getBean(clz);
+ return result;
+ }
+
+ /**
+ * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+ *
+ * @param name
+ * @return boolean
+ */
+ public static boolean containsBean(String name)
+ {
+ return beanFactory.containsBean(name);
+ }
+
+ /**
+ * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+ *
+ * @param name
+ * @return boolean
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ *
+ */
+ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException
+ {
+ return beanFactory.isSingleton(name);
+ }
+
+ /**
+ * @param name
+ * @return Class 注册对象的类型
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ *
+ */
+ public static Class<?> getType(String name) throws NoSuchBeanDefinitionException
+ {
+ return beanFactory.getType(name);
+ }
+
+ /**
+ * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+ *
+ * @param name
+ * @return
+ * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
+ *
+ */
+ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException
+ {
+ return beanFactory.getAliases(name);
+ }
+
+ /**
+ * 获取aop代理对象
+ *
+ * @param invoker
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getAopProxy(T invoker)
+ {
+ return (T) AopContext.currentProxy();
+ }
+
+ /**
+ * 获取当前的环境配置,无配置返回null
+ *
+ * @return 当前的环境配置
+ */
+ public static String[] getActiveProfiles()
+ {
+ return applicationContext.getEnvironment().getActiveProfiles();
+ }
+
+ /**
+ * 获取当前的环境配置,当有多个环境配置时,只获取第一个
+ *
+ * @return 当前的环境配置
+ */
+ public static String getActiveProfile()
+ {
+ final String[] activeProfiles = getActiveProfiles();
+ return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;
+ }
+
+ /**
+ * 获取配置文件中的值
+ *
+ * @param key 配置文件的key
+ * @return 当前的配置文件的值
+ *
+ */
+ public static String getRequiredProperty(String key)
+ {
+ return applicationContext.getEnvironment().getRequiredProperty(key);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java
new file mode 100644
index 0000000..451c7dc
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java
@@ -0,0 +1,70 @@
+package com.ruoyi.common.utils.sql;
+
+import com.ruoyi.common.exception.UtilException;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * sql操作工具类
+ *
+ * @author ruoyi
+ */
+public class SqlUtil
+{
+ /**
+ * 定义常用的 sql关键字
+ */
+ public static String SQL_REGEX = "and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
+
+ /**
+ * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
+ */
+ public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+ /**
+ * 限制orderBy最大长度
+ */
+ private static final int ORDER_BY_MAX_LENGTH = 500;
+
+ /**
+ * 检查字符,防止注入绕过
+ */
+ public static String escapeOrderBySql(String value)
+ {
+ if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value))
+ {
+ throw new UtilException("参数不符合规范,不能进行查询");
+ }
+ if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH)
+ {
+ throw new UtilException("参数已超过最大限制,不能进行查询");
+ }
+ return value;
+ }
+
+ /**
+ * 验证 order by 语法是否符合规范
+ */
+ public static boolean isValidOrderBySql(String value)
+ {
+ return value.matches(SQL_PATTERN);
+ }
+
+ /**
+ * SQL关键字检查
+ */
+ public static void filterKeyword(String value)
+ {
+ if (StringUtils.isEmpty(value))
+ {
+ return;
+ }
+ String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+ for (String sqlKeyword : sqlKeywords)
+ {
+ if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1)
+ {
+ throw new UtilException("参数存在SQL注入风险");
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java
new file mode 100644
index 0000000..2c84427
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java
@@ -0,0 +1,49 @@
+package com.ruoyi.common.utils.uuid;
+
+/**
+ * ID生成器工具类
+ *
+ * @author ruoyi
+ */
+public class IdUtils
+{
+ /**
+ * 获取随机UUID
+ *
+ * @return 随机UUID
+ */
+ public static String randomUUID()
+ {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * 简化的UUID,去掉了横线
+ *
+ * @return 简化的UUID,去掉了横线
+ */
+ public static String simpleUUID()
+ {
+ return UUID.randomUUID().toString(true);
+ }
+
+ /**
+ * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID
+ *
+ * @return 随机UUID
+ */
+ public static String fastUUID()
+ {
+ return UUID.fastUUID().toString();
+ }
+
+ /**
+ * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID
+ *
+ * @return 简化的UUID,去掉了横线
+ */
+ public static String fastSimpleUUID()
+ {
+ return UUID.fastUUID().toString(true);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java
new file mode 100644
index 0000000..bf99611
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java
@@ -0,0 +1,86 @@
+package com.ruoyi.common.utils.uuid;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * @author ruoyi 序列生成类
+ */
+public class Seq
+{
+ // 通用序列类型
+ public static final String commSeqType = "COMMON";
+
+ // 上传序列类型
+ public static final String uploadSeqType = "UPLOAD";
+
+ // 通用接口序列数
+ private static AtomicInteger commSeq = new AtomicInteger(1);
+
+ // 上传接口序列数
+ private static AtomicInteger uploadSeq = new AtomicInteger(1);
+
+ // 机器标识
+ private static final String machineCode = "A";
+
+ /**
+ * 获取通用序列号
+ *
+ * @return 序列值
+ */
+ public static String getId()
+ {
+ return getId(commSeqType);
+ }
+
+ /**
+ * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串
+ *
+ * @return 序列值
+ */
+ public static String getId(String type)
+ {
+ AtomicInteger atomicInt = commSeq;
+ if (uploadSeqType.equals(type))
+ {
+ atomicInt = uploadSeq;
+ }
+ return getId(atomicInt, 3);
+ }
+
+ /**
+ * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串
+ *
+ * @param atomicInt 序列数
+ * @param length 数值长度
+ * @return 序列值
+ */
+ public static String getId(AtomicInteger atomicInt, int length)
+ {
+ String result = DateUtils.dateTimeNow();
+ result += machineCode;
+ result += getSeq(atomicInt, length);
+ return result;
+ }
+
+ /**
+ * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数
+ *
+ * @return 序列值
+ */
+ private synchronized static String getSeq(AtomicInteger atomicInt, int length)
+ {
+ // 先取值再+1
+ int value = atomicInt.getAndIncrement();
+
+ // 如果更新后值>=10 的 (length)幂次方则重置为1
+ int maxSeq = (int) Math.pow(10, length);
+ if (atomicInt.get() >= maxSeq)
+ {
+ atomicInt.set(1);
+ }
+ // 转字符串,用0左补齐
+ return StringUtils.padl(value, length);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java
new file mode 100644
index 0000000..a5585d6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java
@@ -0,0 +1,484 @@
+package com.ruoyi.common.utils.uuid;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import com.ruoyi.common.exception.UtilException;
+
+/**
+ * 提供通用唯一识别码(universally unique identifier)(UUID)实现
+ *
+ * @author ruoyi
+ */
+public final class UUID implements java.io.Serializable, Comparable<UUID>
+{
+ private static final long serialVersionUID = -1185015143654744140L;
+
+ /**
+ * SecureRandom 的单例
+ *
+ */
+ private static class Holder
+ {
+ static final SecureRandom numberGenerator = getSecureRandom();
+ }
+
+ /** 此UUID的最高64有效位 */
+ private final long mostSigBits;
+
+ /** 此UUID的最低64有效位 */
+ private final long leastSigBits;
+
+ /**
+ * 私有构造
+ *
+ * @param data 数据
+ */
+ private UUID(byte[] data)
+ {
+ long msb = 0;
+ long lsb = 0;
+ assert data.length == 16 : "data must be 16 bytes in length";
+ for (int i = 0; i < 8; i++)
+ {
+ msb = (msb << 8) | (data[i] & 0xff);
+ }
+ for (int i = 8; i < 16; i++)
+ {
+ lsb = (lsb << 8) | (data[i] & 0xff);
+ }
+ this.mostSigBits = msb;
+ this.leastSigBits = lsb;
+ }
+
+ /**
+ * 使用指定的数据构造新的 UUID。
+ *
+ * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位
+ * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位
+ */
+ public UUID(long mostSigBits, long leastSigBits)
+ {
+ this.mostSigBits = mostSigBits;
+ this.leastSigBits = leastSigBits;
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。
+ *
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID fastUUID()
+ {
+ return randomUUID(false);
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+ *
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID randomUUID()
+ {
+ return randomUUID(true);
+ }
+
+ /**
+ * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+ *
+ * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能
+ * @return 随机生成的 {@code UUID}
+ */
+ public static UUID randomUUID(boolean isSecure)
+ {
+ final Random ng = isSecure ? Holder.numberGenerator : getRandom();
+
+ byte[] randomBytes = new byte[16];
+ ng.nextBytes(randomBytes);
+ randomBytes[6] &= 0x0f; /* clear version */
+ randomBytes[6] |= 0x40; /* set to version 4 */
+ randomBytes[8] &= 0x3f; /* clear variant */
+ randomBytes[8] |= 0x80; /* set to IETF variant */
+ return new UUID(randomBytes);
+ }
+
+ /**
+ * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。
+ *
+ * @param name 用于构造 UUID 的字节数组。
+ *
+ * @return 根据指定数组生成的 {@code UUID}
+ */
+ public static UUID nameUUIDFromBytes(byte[] name)
+ {
+ MessageDigest md;
+ try
+ {
+ md = MessageDigest.getInstance("MD5");
+ }
+ catch (NoSuchAlgorithmException nsae)
+ {
+ throw new InternalError("MD5 not supported");
+ }
+ byte[] md5Bytes = md.digest(name);
+ md5Bytes[6] &= 0x0f; /* clear version */
+ md5Bytes[6] |= 0x30; /* set to version 3 */
+ md5Bytes[8] &= 0x3f; /* clear variant */
+ md5Bytes[8] |= 0x80; /* set to IETF variant */
+ return new UUID(md5Bytes);
+ }
+
+ /**
+ * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。
+ *
+ * @param name 指定 {@code UUID} 字符串
+ * @return 具有指定值的 {@code UUID}
+ * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常
+ *
+ */
+ public static UUID fromString(String name)
+ {
+ String[] components = name.split("-");
+ if (components.length != 5)
+ {
+ throw new IllegalArgumentException("Invalid UUID string: " + name);
+ }
+ for (int i = 0; i < 5; i++)
+ {
+ components[i] = "0x" + components[i];
+ }
+
+ long mostSigBits = Long.decode(components[0]).longValue();
+ mostSigBits <<= 16;
+ mostSigBits |= Long.decode(components[1]).longValue();
+ mostSigBits <<= 16;
+ mostSigBits |= Long.decode(components[2]).longValue();
+
+ long leastSigBits = Long.decode(components[3]).longValue();
+ leastSigBits <<= 48;
+ leastSigBits |= Long.decode(components[4]).longValue();
+
+ return new UUID(mostSigBits, leastSigBits);
+ }
+
+ /**
+ * 返回此 UUID 的 128 位值中的最低有效 64 位。
+ *
+ * @return 此 UUID 的 128 位值中的最低有效 64 位。
+ */
+ public long getLeastSignificantBits()
+ {
+ return leastSigBits;
+ }
+
+ /**
+ * 返回此 UUID 的 128 位值中的最高有效 64 位。
+ *
+ * @return 此 UUID 的 128 位值中最高有效 64 位。
+ */
+ public long getMostSignificantBits()
+ {
+ return mostSigBits;
+ }
+
+ /**
+ * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。
+ * <p>
+ * 版本号具有以下含意:
+ * <ul>
+ * <li>1 基于时间的 UUID
+ * <li>2 DCE 安全 UUID
+ * <li>3 基于名称的 UUID
+ * <li>4 随机生成的 UUID
+ * </ul>
+ *
+ * @return 此 {@code UUID} 的版本号
+ */
+ public int version()
+ {
+ // Version is bits masked by 0x000000000000F000 in MS long
+ return (int) ((mostSigBits >> 12) & 0x0f);
+ }
+
+ /**
+ * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。
+ * <p>
+ * 变体号具有以下含意:
+ * <ul>
+ * <li>0 为 NCS 向后兼容保留
+ * <li>2 <a href="http://www.ietf.org/rfc/rfc4122.txt">IETF RFC 4122</a>(Leach-Salz), 用于此类
+ * <li>6 保留,微软向后兼容
+ * <li>7 保留供以后定义使用
+ * </ul>
+ *
+ * @return 此 {@code UUID} 相关联的变体号
+ */
+ public int variant()
+ {
+ // This field is composed of a varying number of bits.
+ // 0 - - Reserved for NCS backward compatibility
+ // 1 0 - The IETF aka Leach-Salz variant (used by this class)
+ // 1 1 0 Reserved, Microsoft backward compatibility
+ // 1 1 1 Reserved for future definition.
+ return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));
+ }
+
+ /**
+ * 与此 UUID 相关联的时间戳值。
+ *
+ * <p>
+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br>
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。
+ *
+ * <p>
+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+ *
+ * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。
+ */
+ public long timestamp() throws UnsupportedOperationException
+ {
+ checkTimeBase();
+ return (mostSigBits & 0x0FFFL) << 48//
+ | ((mostSigBits >> 16) & 0x0FFFFL) << 32//
+ | mostSigBits >>> 32;
+ }
+
+ /**
+ * 与此 UUID 相关联的时钟序列值。
+ *
+ * <p>
+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。
+ * <p>
+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出
+ * UnsupportedOperationException。
+ *
+ * @return 此 {@code UUID} 的时钟序列
+ *
+ * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+ */
+ public int clockSequence() throws UnsupportedOperationException
+ {
+ checkTimeBase();
+ return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);
+ }
+
+ /**
+ * 与此 UUID 相关的节点值。
+ *
+ * <p>
+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。
+ * <p>
+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+ *
+ * @return 此 {@code UUID} 的节点值
+ *
+ * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+ */
+ public long node() throws UnsupportedOperationException
+ {
+ checkTimeBase();
+ return leastSigBits & 0x0000FFFFFFFFFFFFL;
+ }
+
+ /**
+ * 返回此{@code UUID} 的字符串表现形式。
+ *
+ * <p>
+ * UUID 的字符串表示形式由此 BNF 描述:
+ *
+ * <pre>
+ * {@code
+ * UUID = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+ * time_low = 4*<hexOctet>
+ * time_mid = 2*<hexOctet>
+ * time_high_and_version = 2*<hexOctet>
+ * variant_and_sequence = 2*<hexOctet>
+ * node = 6*<hexOctet>
+ * hexOctet = <hexDigit><hexDigit>
+ * hexDigit = [0-9a-fA-F]
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * @return 此{@code UUID} 的字符串表现形式
+ * @see #toString(boolean)
+ */
+ @Override
+ public String toString()
+ {
+ return toString(false);
+ }
+
+ /**
+ * 返回此{@code UUID} 的字符串表现形式。
+ *
+ * <p>
+ * UUID 的字符串表示形式由此 BNF 描述:
+ *
+ * <pre>
+ * {@code
+ * UUID = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+ * time_low = 4*<hexOctet>
+ * time_mid = 2*<hexOctet>
+ * time_high_and_version = 2*<hexOctet>
+ * variant_and_sequence = 2*<hexOctet>
+ * node = 6*<hexOctet>
+ * hexOctet = <hexDigit><hexDigit>
+ * hexDigit = [0-9a-fA-F]
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串
+ * @return 此{@code UUID} 的字符串表现形式
+ */
+ public String toString(boolean isSimple)
+ {
+ final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36);
+ // time_low
+ builder.append(digits(mostSigBits >> 32, 8));
+ if (!isSimple)
+ {
+ builder.append('-');
+ }
+ // time_mid
+ builder.append(digits(mostSigBits >> 16, 4));
+ if (!isSimple)
+ {
+ builder.append('-');
+ }
+ // time_high_and_version
+ builder.append(digits(mostSigBits, 4));
+ if (!isSimple)
+ {
+ builder.append('-');
+ }
+ // variant_and_sequence
+ builder.append(digits(leastSigBits >> 48, 4));
+ if (!isSimple)
+ {
+ builder.append('-');
+ }
+ // node
+ builder.append(digits(leastSigBits, 12));
+
+ return builder.toString();
+ }
+
+ /**
+ * 返回此 UUID 的哈希码。
+ *
+ * @return UUID 的哈希码值。
+ */
+ @Override
+ public int hashCode()
+ {
+ long hilo = mostSigBits ^ leastSigBits;
+ return ((int) (hilo >> 32)) ^ (int) hilo;
+ }
+
+ /**
+ * 将此对象与指定对象比较。
+ * <p>
+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。
+ *
+ * @param obj 要与之比较的对象
+ *
+ * @return 如果对象相同,则返回 {@code true};否则返回 {@code false}
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if ((null == obj) || (obj.getClass() != UUID.class))
+ {
+ return false;
+ }
+ UUID id = (UUID) obj;
+ return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);
+ }
+
+ // Comparison Operations
+
+ /**
+ * 将此 UUID 与指定的 UUID 比较。
+ *
+ * <p>
+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。
+ *
+ * @param val 与此 UUID 比较的 UUID
+ *
+ * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。
+ *
+ */
+ @Override
+ public int compareTo(UUID val)
+ {
+ // The ordering is intentionally set up so that the UUIDs
+ // can simply be numerically compared as two numbers
+ return (this.mostSigBits < val.mostSigBits ? -1 : //
+ (this.mostSigBits > val.mostSigBits ? 1 : //
+ (this.leastSigBits < val.leastSigBits ? -1 : //
+ (this.leastSigBits > val.leastSigBits ? 1 : //
+ 0))));
+ }
+
+ // -------------------------------------------------------------------------------------------------------------------
+ // Private method start
+ /**
+ * 返回指定数字对应的hex值
+ *
+ * @param val 值
+ * @param digits 位
+ * @return 值
+ */
+ private static String digits(long val, int digits)
+ {
+ long hi = 1L << (digits * 4);
+ return Long.toHexString(hi | (val & (hi - 1))).substring(1);
+ }
+
+ /**
+ * 检查是否为time-based版本UUID
+ */
+ private void checkTimeBase()
+ {
+ if (version() != 1)
+ {
+ throw new UnsupportedOperationException("Not a time-based UUID");
+ }
+ }
+
+ /**
+ * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+ *
+ * @return {@link SecureRandom}
+ */
+ public static SecureRandom getSecureRandom()
+ {
+ try
+ {
+ return SecureRandom.getInstance("SHA1PRNG");
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new UtilException(e);
+ }
+ }
+
+ /**
+ * 获取随机数生成器对象<br>
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。
+ *
+ * @return {@link ThreadLocalRandom}
+ */
+ public static ThreadLocalRandom getRandom()
+ {
+ return ThreadLocalRandom.current();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java
new file mode 100644
index 0000000..cb4b408
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java
@@ -0,0 +1,27 @@
+package com.ruoyi.common.xss;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义xss校验注解
+ *
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER })
+@Constraint(validatedBy = { XssValidator.class })
+public @interface Xss
+{
+ String message()
+
+ default "不允许任何脚本运行";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java
new file mode 100644
index 0000000..7c8e4c0
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java
@@ -0,0 +1,39 @@
+package com.ruoyi.common.xss;
+
+import com.ruoyi.common.utils.StringUtils;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 自定义xss校验注解实现
+ *
+ * @author ruoyi
+ */
+public class XssValidator implements ConstraintValidator<Xss, String>
+{
+ private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />";
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext)
+ {
+ if (StringUtils.isBlank(value))
+ {
+ return true;
+ }
+ return !containsHtml(value);
+ }
+
+ public static boolean containsHtml(String value)
+ {
+ StringBuilder sHtml = new StringBuilder();
+ Pattern pattern = Pattern.compile(HTML_PATTERN);
+ Matcher matcher = pattern.matcher(value);
+ while (matcher.find())
+ {
+ sHtml.append(matcher.group());
+ }
+ return pattern.matcher(sHtml).matches();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml
new file mode 100644
index 0000000..17cf509
--- /dev/null
+++ b/ruoyi-framework/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-framework</artifactId>
+
+ <description>
+ framework框架核心
+ </description>
+
+ <dependencies>
+
+ <!-- SpringBoot Web容器 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <!-- SpringBoot 拦截器 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-aop</artifactId>
+ </dependency>
+
+ <!-- 阿里数据库连接池 -->
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>druid-spring-boot-3-starter</artifactId>
+ </dependency>
+
+ <!-- 验证码 -->
+ <dependency>
+ <groupId>pro.fessional</groupId>
+ <artifactId>kaptcha</artifactId>
+ <exclusions>
+ <exclusion>
+ <artifactId>servlet-api</artifactId>
+ <groupId>javax.servlet</groupId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- 获取系统信息 -->
+ <dependency>
+ <groupId>com.github.oshi</groupId>
+ <artifactId>oshi-core</artifactId>
+ </dependency>
+
+ <!-- 系统模块-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-system</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java
new file mode 100644
index 0000000..b2337c9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java
@@ -0,0 +1,184 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.DataScope;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.context.PermissionContextHolder;
+
+/**
+ * 数据过滤处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+ /**
+ * 全部数据权限
+ */
+ public static final String DATA_SCOPE_ALL = "1";
+
+ /**
+ * 自定数据权限
+ */
+ public static final String DATA_SCOPE_CUSTOM = "2";
+
+ /**
+ * 部门数据权限
+ */
+ public static final String DATA_SCOPE_DEPT = "3";
+
+ /**
+ * 部门及以下数据权限
+ */
+ public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+ /**
+ * 仅本人数据权限
+ */
+ public static final String DATA_SCOPE_SELF = "5";
+
+ /**
+ * 数据权限过滤关键字
+ */
+ public static final String DATA_SCOPE = "dataScope";
+
+ @Before("@annotation(controllerDataScope)")
+ public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
+ {
+ clearDataScope(point);
+ handleDataScope(point, controllerDataScope);
+ }
+
+ protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
+ {
+ // 获取当前的用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNotNull(loginUser))
+ {
+ SysUser currentUser = loginUser.getUser();
+ // 如果是超级管理员,则不过滤数据
+ if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+ {
+ String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
+ dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission);
+ }
+ }
+ }
+
+ /**
+ * 数据范围过滤
+ *
+ * @param joinPoint 切点
+ * @param user 用户
+ * @param deptAlias 部门别名
+ * @param userAlias 用户别名
+ * @param permission 权限字符
+ */
+ public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
+ {
+ StringBuilder sqlString = new StringBuilder();
+ List<String> conditions = new ArrayList<String>();
+ List<String> scopeCustomIds = new ArrayList<String>();
+ user.getRoles().forEach(role -> {
+ if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
+ {
+ scopeCustomIds.add(Convert.toStr(role.getRoleId()));
+ }
+ });
+
+ for (SysRole role : user.getRoles())
+ {
+ String dataScope = role.getDataScope();
+ if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE))
+ {
+ continue;
+ }
+ if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
+ {
+ continue;
+ }
+ if (DATA_SCOPE_ALL.equals(dataScope))
+ {
+ sqlString = new StringBuilder();
+ conditions.add(dataScope);
+ break;
+ }
+ else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+ {
+ if (scopeCustomIds.size() > 1)
+ {
+ // 多个自定数据权限使用in查询,避免多次拼接。
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds)));
+ }
+ else
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId()));
+ }
+ }
+ else if (DATA_SCOPE_DEPT.equals(dataScope))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+ }
+ else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId()));
+ }
+ else if (DATA_SCOPE_SELF.equals(dataScope))
+ {
+ if (StringUtils.isNotBlank(userAlias))
+ {
+ sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+ }
+ else
+ {
+ // 数据权限为仅本人且没有userAlias别名不查询任何数据
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+ }
+ conditions.add(dataScope);
+ }
+
+ // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据
+ if (StringUtils.isEmpty(conditions))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+
+ if (StringUtils.isNotBlank(sqlString.toString()))
+ {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+ {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+ }
+ }
+ }
+
+ /**
+ * 拼接权限sql前先清空params.dataScope参数防止注入
+ */
+ private void clearDataScope(final JoinPoint joinPoint)
+ {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+ {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, "");
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java
new file mode 100644
index 0000000..8c2c9f4
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java
@@ -0,0 +1,72 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.Objects;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.DataSource;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;
+
+/**
+ * 多数据源处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+ protected Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ + "|| @within(com.ruoyi.common.annotation.DataSource)")
+ public void dsPointCut()
+ {
+
+ }
+
+ @Around("dsPointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable
+ {
+ DataSource dataSource = getDataSource(point);
+
+ if (StringUtils.isNotNull(dataSource))
+ {
+ DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+ }
+
+ try
+ {
+ return point.proceed();
+ }
+ finally
+ {
+ // 销毁数据源 在执行方法之后
+ DynamicDataSourceContextHolder.clearDataSourceType();
+ }
+ }
+
+ /**
+ * 获取需要切换的数据源
+ */
+ public DataSource getDataSource(ProceedingJoinPoint point)
+ {
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+ if (Objects.nonNull(dataSource))
+ {
+ return dataSource;
+ }
+
+ return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
new file mode 100644
index 0000000..7f59d50
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
@@ -0,0 +1,255 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.Collection;
+import java.util.Map;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.NamedThreadLocal;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.BusinessStatus;
+import com.ruoyi.common.enums.HttpMethod;
+import com.ruoyi.common.filter.PropertyPreExcludeFilter;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.system.domain.SysOperLog;
+
+/**
+ * 操作日志记录处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+ private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+ /** 排除敏感属性字段 */
+ public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
+
+ /** 计算操作消耗时间 */
+ private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
+
+ /**
+ * 处理请求前执行
+ */
+ @Before(value = "@annotation(controllerLog)")
+ public void boBefore(JoinPoint joinPoint, Log controllerLog)
+ {
+ TIME_THREADLOCAL.set(System.currentTimeMillis());
+ }
+
+ /**
+ * 处理完请求后执行
+ *
+ * @param joinPoint 切点
+ */
+ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
+ public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
+ {
+ handleLog(joinPoint, controllerLog, null, jsonResult);
+ }
+
+ /**
+ * 拦截异常操作
+ *
+ * @param joinPoint 切点
+ * @param e 异常
+ */
+ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
+ public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
+ {
+ handleLog(joinPoint, controllerLog, e, null);
+ }
+
+ protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
+ {
+ try
+ {
+ // 获取当前的用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+
+ // *========数据库日志=========*//
+ SysOperLog operLog = new SysOperLog();
+ operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+ // 请求的地址
+ String ip = IpUtils.getIpAddr();
+ operLog.setOperIp(ip);
+ operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
+ if (loginUser != null)
+ {
+ operLog.setOperName(loginUser.getUsername());
+ SysUser currentUser = loginUser.getUser();
+ if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
+ {
+ operLog.setDeptName(currentUser.getDept().getDeptName());
+ }
+ }
+
+ if (e != null)
+ {
+ operLog.setStatus(BusinessStatus.FAIL.ordinal());
+ operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+ }
+ // 设置方法名称
+ String className = joinPoint.getTarget().getClass().getName();
+ String methodName = joinPoint.getSignature().getName();
+ operLog.setMethod(className + "." + methodName + "()");
+ // 设置请求方式
+ operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+ // 处理设置注解上的参数
+ getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
+ // 设置消耗时间
+ operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
+ // 保存数据库
+ AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+ }
+ catch (Exception exp)
+ {
+ // 记录本地异常日志
+ log.error("异常信息:{}", exp.getMessage());
+ exp.printStackTrace();
+ }
+ finally
+ {
+ TIME_THREADLOCAL.remove();
+ }
+ }
+
+ /**
+ * 获取注解中对方法的描述信息 用于Controller层注解
+ *
+ * @param log 日志
+ * @param operLog 操作日志
+ * @throws Exception
+ */
+ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
+ {
+ // 设置action动作
+ operLog.setBusinessType(log.businessType().ordinal());
+ // 设置标题
+ operLog.setTitle(log.title());
+ // 设置操作人类别
+ operLog.setOperatorType(log.operatorType().ordinal());
+ // 是否需要保存request,参数和值
+ if (log.isSaveRequestData())
+ {
+ // 获取参数的信息,传入到数据库中。
+ setRequestValue(joinPoint, operLog, log.excludeParamNames());
+ }
+ // 是否需要保存response,参数和值
+ if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
+ {
+ operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
+ }
+ }
+
+ /**
+ * 获取请求的参数,放到log中
+ *
+ * @param operLog 操作日志
+ * @throws Exception 异常
+ */
+ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
+ {
+ Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
+ String requestMethod = operLog.getRequestMethod();
+ if (StringUtils.isEmpty(paramsMap)
+ && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
+ {
+ String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
+ operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+ }
+ else
+ {
+ operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
+ }
+ }
+
+ /**
+ * 参数拼装
+ */
+ private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames)
+ {
+ String params = "";
+ if (paramsArray != null && paramsArray.length > 0)
+ {
+ for (Object o : paramsArray)
+ {
+ if (StringUtils.isNotNull(o) && !isFilterObject(o))
+ {
+ try
+ {
+ String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
+ params += jsonObj.toString() + " ";
+ }
+ catch (Exception e)
+ {
+ }
+ }
+ }
+ }
+ return params.trim();
+ }
+
+ /**
+ * 忽略敏感属性
+ */
+ public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames)
+ {
+ return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
+ }
+
+ /**
+ * 判断是否需要过滤的对象。
+ *
+ * @param o 对象信息。
+ * @return 如果是需要过滤的对象,则返回true;否则返回false。
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean isFilterObject(final Object o)
+ {
+ Class<?> clazz = o.getClass();
+ if (clazz.isArray())
+ {
+ return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+ }
+ else if (Collection.class.isAssignableFrom(clazz))
+ {
+ Collection collection = (Collection) o;
+ for (Object value : collection)
+ {
+ return value instanceof MultipartFile;
+ }
+ }
+ else if (Map.class.isAssignableFrom(clazz))
+ {
+ Map map = (Map) o;
+ for (Object value : map.entrySet())
+ {
+ Map.Entry entry = (Map.Entry) value;
+ return entry.getValue() instanceof MultipartFile;
+ }
+ }
+ return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+ || o instanceof BindingResult;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
new file mode 100644
index 0000000..b720bc1
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
@@ -0,0 +1,89 @@
+package com.ruoyi.framework.aspectj;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.RateLimiter;
+import com.ruoyi.common.enums.LimitType;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+
+/**
+ * 限流处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+ private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+ private RedisTemplate<Object, Object> redisTemplate;
+
+ private RedisScript<Long> limitScript;
+
+ @Autowired
+ public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+ {
+ this.redisTemplate = redisTemplate;
+ }
+
+ @Autowired
+ public void setLimitScript(RedisScript<Long> limitScript)
+ {
+ this.limitScript = limitScript;
+ }
+
+ @Before("@annotation(rateLimiter)")
+ public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
+ {
+ int time = rateLimiter.time();
+ int count = rateLimiter.count();
+
+ String combineKey = getCombineKey(rateLimiter, point);
+ List<Object> keys = Collections.singletonList(combineKey);
+ try
+ {
+ Long number = redisTemplate.execute(limitScript, keys, count, time);
+ if (StringUtils.isNull(number) || number.intValue() > count)
+ {
+ throw new ServiceException("访问过于频繁,请稍候再试");
+ }
+ log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
+ }
+ catch (ServiceException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("服务器限流异常,请稍候再试");
+ }
+ }
+
+ public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+ {
+ StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+ if (rateLimiter.limitType() == LimitType.IP)
+ {
+ stringBuffer.append(IpUtils.getIpAddr()).append("-");
+ }
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ Method method = signature.getMethod();
+ Class<?> targetClass = method.getDeclaringClass();
+ stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
+ return stringBuffer.toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java
new file mode 100644
index 0000000..1d4dc1f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java
@@ -0,0 +1,30 @@
+package com.ruoyi.framework.config;
+
+import java.util.TimeZone;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+/**
+ * 程序注解配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.ruoyi.**.mapper")
+public class ApplicationConfig
+{
+ /**
+ * 时区配置
+ */
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+ {
+ return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java
new file mode 100644
index 0000000..43e78ae
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java
@@ -0,0 +1,83 @@
+package com.ruoyi.framework.config;
+
+import java.util.Properties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * 验证码配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class CaptchaConfig
+{
+ @Bean(name = "captchaProducer")
+ public DefaultKaptcha getKaptchaBean()
+ {
+ DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+ Properties properties = new Properties();
+ // 是否有边框 默认为true 我们可以自己设置yes,no
+ properties.setProperty(KAPTCHA_BORDER, "yes");
+ // 验证码文本字符颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+ // 验证码图片宽度 默认为200
+ properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+ // 验证码图片高度 默认为50
+ properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+ // 验证码文本字符大小 默认为40
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+ // KAPTCHA_SESSION_KEY
+ properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+ // 验证码文本字符长度 默认为5
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+ // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+ // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+ properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+ Config config = new Config(properties);
+ defaultKaptcha.setConfig(config);
+ return defaultKaptcha;
+ }
+
+ @Bean(name = "captchaProducerMath")
+ public DefaultKaptcha getKaptchaBeanMath()
+ {
+ DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+ Properties properties = new Properties();
+ // 是否有边框 默认为true 我们可以自己设置yes,no
+ properties.setProperty(KAPTCHA_BORDER, "yes");
+ // 边框颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+ // 验证码文本字符颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+ // 验证码图片宽度 默认为200
+ properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+ // 验证码图片高度 默认为50
+ properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+ // 验证码文本字符大小 默认为40
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+ // KAPTCHA_SESSION_KEY
+ properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+ // 验证码文本生成器
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");
+ // 验证码文本字符间距 默认为2
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+ // 验证码文本字符长度 默认为5
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+ // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+ // 验证码噪点颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+ // 干扰实现类
+ properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+ // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+ properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+ Config config = new Config(properties);
+ defaultKaptcha.setConfig(config);
+ return defaultKaptcha;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java
new file mode 100644
index 0000000..d9cb8c5
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java
@@ -0,0 +1,126 @@
+package com.ruoyi.framework.config;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.sql.DataSource;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
+import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.ruoyi.common.enums.DataSourceType;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.framework.config.properties.DruidProperties;
+import com.ruoyi.framework.datasource.DynamicDataSource;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+/**
+ * druid 配置多数据源
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class DruidConfig
+{
+ @Bean
+ @ConfigurationProperties("spring.datasource.druid.master")
+ public DataSource masterDataSource(DruidProperties druidProperties)
+ {
+ DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+ return druidProperties.dataSource(dataSource);
+ }
+
+ @Bean
+ @ConfigurationProperties("spring.datasource.druid.slave")
+ @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
+ public DataSource slaveDataSource(DruidProperties druidProperties)
+ {
+ DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+ return druidProperties.dataSource(dataSource);
+ }
+
+ @Bean(name = "dynamicDataSource")
+ @Primary
+ public DynamicDataSource dataSource(DataSource masterDataSource)
+ {
+ Map<Object, Object> targetDataSources = new HashMap<>();
+ targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+ setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+ return new DynamicDataSource(masterDataSource, targetDataSources);
+ }
+
+ /**
+ * 设置数据源
+ *
+ * @param targetDataSources 备选数据源集合
+ * @param sourceName 数据源名称
+ * @param beanName bean名称
+ */
+ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
+ {
+ try
+ {
+ DataSource dataSource = SpringUtils.getBean(beanName);
+ targetDataSources.put(sourceName, dataSource);
+ }
+ catch (Exception e)
+ {
+ }
+ }
+
+ /**
+ * 去除监控页面底部的广告
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+ public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+ {
+ // 获取web监控页面的参数
+ DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+ // 提取common.js的配置路径
+ String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+ String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+ final String filePath = "support/http/resources/js/common.js";
+ // 创建filter进行过滤
+ Filter filter = new Filter()
+ {
+ @Override
+ public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException
+ {
+ }
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException
+ {
+ chain.doFilter(request, response);
+ // 重置缓冲区,响应头不会被重置
+ response.resetBuffer();
+ // 获取common.js
+ String text = Utils.readFromResource(filePath);
+ // 正则替换banner, 除去底部的广告信息
+ text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+ text = text.replaceAll("powered.*?shrek.wang</a>", "");
+ response.getWriter().write(text);
+ }
+ @Override
+ public void destroy()
+ {
+ }
+ };
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+ registrationBean.setFilter(filter);
+ registrationBean.addUrlPatterns(commonJsPattern);
+ return registrationBean;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java
new file mode 100644
index 0000000..4adbb7f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java
@@ -0,0 +1,52 @@
+package com.ruoyi.framework.config;
+
+import java.nio.charset.Charset;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import com.alibaba.fastjson2.filter.Filter;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * Redis使用FastJson序列化
+ *
+ * @author ruoyi
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+ public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+ static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
+
+ private Class<T> clazz;
+
+ public FastJson2JsonRedisSerializer(Class<T> clazz)
+ {
+ super();
+ this.clazz = clazz;
+ }
+
+ @Override
+ public byte[] serialize(T t) throws SerializationException
+ {
+ if (t == null)
+ {
+ return new byte[0];
+ }
+ return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
+ }
+
+ @Override
+ public T deserialize(byte[] bytes) throws SerializationException
+ {
+ if (bytes == null || bytes.length <= 0)
+ {
+ return null;
+ }
+ String str = new String(bytes, DEFAULT_CHARSET);
+
+ return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java
new file mode 100644
index 0000000..fad6bb8
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java
@@ -0,0 +1,58 @@
+package com.ruoyi.framework.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import jakarta.servlet.DispatcherType;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.ruoyi.common.filter.RepeatableFilter;
+import com.ruoyi.common.filter.XssFilter;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * Filter配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class FilterConfig
+{
+ @Value("${xss.excludes}")
+ private String excludes;
+
+ @Value("${xss.urlPatterns}")
+ private String urlPatterns;
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+ public FilterRegistrationBean xssFilterRegistration()
+ {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ registration.setDispatcherTypes(DispatcherType.REQUEST);
+ registration.setFilter(new XssFilter());
+ registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+ registration.setName("xssFilter");
+ registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+ Map<String, String> initParameters = new HashMap<String, String>();
+ initParameters.put("excludes", excludes);
+ registration.setInitParameters(initParameters);
+ return registration;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ public FilterRegistrationBean someFilterRegistration()
+ {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ registration.setFilter(new RepeatableFilter());
+ registration.addUrlPatterns("/*");
+ registration.setName("repeatableFilter");
+ registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+ return registration;
+ }
+
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java
new file mode 100644
index 0000000..163fd01
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java
@@ -0,0 +1,43 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * 资源文件配置加载
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class I18nConfig implements WebMvcConfigurer
+{
+ @Bean
+ public LocaleResolver localeResolver()
+ {
+ SessionLocaleResolver slr = new SessionLocaleResolver();
+ // 默认语言
+ slr.setDefaultLocale(Constants.DEFAULT_LOCALE);
+ return slr;
+ }
+
+ @Bean
+ public LocaleChangeInterceptor localeChangeInterceptor()
+ {
+ LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
+ // 参数名
+ lci.setParamName("lang");
+ return lci;
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry)
+ {
+ registry.addInterceptor(localeChangeInterceptor());
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java
new file mode 100644
index 0000000..7f8e1d5
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java
@@ -0,0 +1,68 @@
+package com.ruoyi.framework.config;
+
+import java.util.Random;
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+/**
+ * 验证码文本生成器
+ *
+ * @author ruoyi
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+ private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+ @Override
+ public String getText()
+ {
+ Integer result = 0;
+ Random random = new Random();
+ int x = random.nextInt(10);
+ int y = random.nextInt(10);
+ StringBuilder suChinese = new StringBuilder();
+ int randomoperands = random.nextInt(3);
+ if (randomoperands == 0)
+ {
+ result = x * y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("*");
+ suChinese.append(CNUMBERS[y]);
+ }
+ else if (randomoperands == 1)
+ {
+ if ((x != 0) && y % x == 0)
+ {
+ result = y / x;
+ suChinese.append(CNUMBERS[y]);
+ suChinese.append("/");
+ suChinese.append(CNUMBERS[x]);
+ }
+ else
+ {
+ result = x + y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("+");
+ suChinese.append(CNUMBERS[y]);
+ }
+ }
+ else
+ {
+ if (x >= y)
+ {
+ result = x - y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("-");
+ suChinese.append(CNUMBERS[y]);
+ }
+ else
+ {
+ result = y - x;
+ suChinese.append(CNUMBERS[y]);
+ suChinese.append("-");
+ suChinese.append(CNUMBERS[x]);
+ }
+ }
+ suChinese.append("=?@" + result);
+ return suChinese.toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
new file mode 100644
index 0000000..057c941
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
@@ -0,0 +1,132 @@
+package com.ruoyi.framework.config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import javax.sql.DataSource;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class MyBatisConfig
+{
+ @Autowired
+ private Environment env;
+
+ static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+ public static String setTypeAliasesPackage(String typeAliasesPackage)
+ {
+ ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+ MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+ List<String> allResult = new ArrayList<String>();
+ try
+ {
+ for (String aliasesPackage : typeAliasesPackage.split(","))
+ {
+ List<String> result = new ArrayList<String>();
+ aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+ Resource[] resources = resolver.getResources(aliasesPackage);
+ if (resources != null && resources.length > 0)
+ {
+ MetadataReader metadataReader = null;
+ for (Resource resource : resources)
+ {
+ if (resource.isReadable())
+ {
+ metadataReader = metadataReaderFactory.getMetadataReader(resource);
+ try
+ {
+ result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+ }
+ catch (ClassNotFoundException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ if (result.size() > 0)
+ {
+ HashSet<String> hashResult = new HashSet<String>(result);
+ allResult.addAll(hashResult);
+ }
+ }
+ if (allResult.size() > 0)
+ {
+ typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+ }
+ else
+ {
+ throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+ }
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ return typeAliasesPackage;
+ }
+
+ public Resource[] resolveMapperLocations(String[] mapperLocations)
+ {
+ ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+ List<Resource> resources = new ArrayList<Resource>();
+ if (mapperLocations != null)
+ {
+ for (String mapperLocation : mapperLocations)
+ {
+ try
+ {
+ Resource[] mappers = resourceResolver.getResources(mapperLocation);
+ resources.addAll(Arrays.asList(mappers));
+ }
+ catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+ return resources.toArray(new Resource[resources.size()]);
+ }
+
+ @Bean
+ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+ {
+ String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+ String mapperLocations = env.getProperty("mybatis.mapperLocations");
+ String configLocation = env.getProperty("mybatis.configLocation");
+ typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+ VFS.addImplClass(SpringBootVFS.class);
+
+ final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+ sessionFactory.setDataSource(dataSource);
+ sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+ sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+ sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+ return sessionFactory.getObject();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
new file mode 100644
index 0000000..3453237
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
@@ -0,0 +1,70 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+ * @author ruoyi
+ */
+@SuppressWarnings("deprecation")
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+ @Bean
+ @SuppressWarnings(value = { "unchecked", "rawtypes" })
+ public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+ {
+ RedisTemplate<Object, Object> template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+ // 使用StringRedisSerializer来序列化和反序列化redis的key值
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(serializer);
+
+ // Hash的key也采用StringRedisSerializer的序列化方式
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(serializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+
+ @Bean
+ public DefaultRedisScript<Long> limitScript()
+ {
+ DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+ redisScript.setScriptText(limitScriptText());
+ redisScript.setResultType(Long.class);
+ return redisScript;
+ }
+
+ /**
+ * 限流脚本
+ */
+ private String limitScriptText()
+ {
+ return "local key = KEYS[1]\n" +
+ "local count = tonumber(ARGV[1])\n" +
+ "local time = tonumber(ARGV[2])\n" +
+ "local current = redis.call('get', key);\n" +
+ "if current and tonumber(current) > count then\n" +
+ " return tonumber(current);\n" +
+ "end\n" +
+ "current = redis.call('incr', key)\n" +
+ "if tonumber(current) == 1 then\n" +
+ " redis.call('expire', key, time)\n" +
+ "end\n" +
+ "return tonumber(current);";
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
new file mode 100644
index 0000000..74fb93e
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
@@ -0,0 +1,73 @@
+package com.ruoyi.framework.config;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.CacheControl;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
+
+/**
+ * 通用配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+ @Autowired
+ private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry)
+ {
+ /** 本地文件上传路径 */
+ registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
+ .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
+
+ /** swagger配置 */
+ registry.addResourceHandler("/swagger-ui/**")
+ .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
+ .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());;
+ }
+
+ /**
+ * 自定义拦截规则
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry)
+ {
+ registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+ }
+
+ /**
+ * 跨域配置
+ */
+ @Bean
+ public CorsFilter corsFilter()
+ {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ // 设置访问源地址
+ config.addAllowedOriginPattern("*");
+ // 设置访问源请求头
+ config.addAllowedHeader("*");
+ // 设置访问源请求方法
+ config.addAllowedMethod("*");
+ // 有效期 1800秒
+ config.setMaxAge(1800L);
+ // 添加映射路径,拦截一切请求
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+ // 返回新的CorsFilter
+ return new CorsFilter(source);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
new file mode 100644
index 0000000..e0be234
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -0,0 +1,139 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.filter.CorsFilter;
+import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
+import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
+import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
+import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
+
+/**
+ * spring security配置
+ *
+ * @author ruoyi
+ */
+@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
+@Configuration
+public class SecurityConfig
+{
+ /**
+ * 自定义用户认证逻辑
+ */
+ @Autowired
+ private UserDetailsService userDetailsService;
+
+ /**
+ * 认证失败处理类
+ */
+ @Autowired
+ private AuthenticationEntryPointImpl unauthorizedHandler;
+
+ /**
+ * 退出处理类
+ */
+ @Autowired
+ private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+ /**
+ * token认证过滤器
+ */
+ @Autowired
+ private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+ /**
+ * 跨域过滤器
+ */
+ @Autowired
+ private CorsFilter corsFilter;
+
+ /**
+ * 允许匿名访问的地址
+ */
+ @Autowired
+ private PermitAllUrlProperties permitAllUrl;
+
+ /**
+ * 身份验证实现
+ */
+ @Bean
+ public AuthenticationManager authenticationManager()
+ {
+ DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
+ daoAuthenticationProvider.setUserDetailsService(userDetailsService);
+ daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
+ return new ProviderManager(daoAuthenticationProvider);
+ }
+
+ /**
+ * anyRequest | 匹配所有请求路径
+ * access | SpringEl表达式结果为true时可以访问
+ * anonymous | 匿名可以访问
+ * denyAll | 用户不能访问
+ * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
+ * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
+ * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
+ * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
+ * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+ * hasRole | 如果有参数,参数表示角色,则其角色可以访问
+ * permitAll | 用户可以任意访问
+ * rememberMe | 允许通过remember-me登录的用户访问
+ * authenticated | 用户登录后可访问
+ */
+ @Bean
+ protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception
+ {
+ return httpSecurity
+ // CSRF禁用,因为不使用session
+ .csrf(csrf -> csrf.disable())
+ // 禁用HTTP响应标头
+ .headers((headersCustomizer) -> {
+ headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
+ })
+ // 认证失败处理类
+ .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
+ // 基于token,所以不需要session
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ // 注解标记允许匿名访问的url
+ .authorizeHttpRequests((requests) -> {
+ permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
+ // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+ requests.requestMatchers("/login", "/register", "/captchaImage","testDownloadTorrent").permitAll()
+ // 静态资源,可匿名访问
+ .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
+ .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
+ // 除上面外的所有请求全部需要鉴权认证
+ .anyRequest().authenticated();
+ })
+ // 添加Logout filter
+ .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
+ // 添加JWT filter
+ .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
+ // 添加CORS filter
+ .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
+ .addFilterBefore(corsFilter, LogoutFilter.class)
+ .build();
+ }
+
+ /**
+ * 强散列哈希加密实现
+ */
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder()
+ {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java
new file mode 100644
index 0000000..cd8fb09
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java
@@ -0,0 +1,32 @@
+package com.ruoyi.framework.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 服务相关配置
+ *
+ * @author ruoyi
+ */
+@Component
+public class ServerConfig
+{
+ /**
+ * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+ *
+ * @return 服务地址
+ */
+ public String getUrl()
+ {
+ HttpServletRequest request = ServletUtils.getRequest();
+ return getDomain(request);
+ }
+
+ public static String getDomain(HttpServletRequest request)
+ {
+ StringBuffer url = request.getRequestURL();
+ String contextPath = request.getServletContext().getContextPath();
+ return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..7840141
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
@@ -0,0 +1,63 @@
+package com.ruoyi.framework.config;
+
+import com.ruoyi.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author ruoyi
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+ // 核心线程池大小
+ private int corePoolSize = 50;
+
+ // 最大可创建的线程数
+ private int maxPoolSize = 200;
+
+ // 队列最大长度
+ private int queueCapacity = 1000;
+
+ // 线程池维护线程所允许的空闲时间
+ private int keepAliveSeconds = 300;
+
+ @Bean(name = "threadPoolTaskExecutor")
+ public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+ {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setCorePoolSize(corePoolSize);
+ executor.setQueueCapacity(queueCapacity);
+ executor.setKeepAliveSeconds(keepAliveSeconds);
+ // 线程池对拒绝任务(无线程可用)的处理策略
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ return executor;
+ }
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService()
+ {
+ return new ScheduledThreadPoolExecutor(corePoolSize,
+ new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+ new ThreadPoolExecutor.CallerRunsPolicy())
+ {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t)
+ {
+ super.afterExecute(r, t);
+ Threads.printException(r, t);
+ }
+ };
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java
new file mode 100644
index 0000000..c8a5c8a
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java
@@ -0,0 +1,89 @@
+package com.ruoyi.framework.config.properties;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import com.alibaba.druid.pool.DruidDataSource;
+
+/**
+ * druid 配置属性
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class DruidProperties
+{
+ @Value("${spring.datasource.druid.initialSize}")
+ private int initialSize;
+
+ @Value("${spring.datasource.druid.minIdle}")
+ private int minIdle;
+
+ @Value("${spring.datasource.druid.maxActive}")
+ private int maxActive;
+
+ @Value("${spring.datasource.druid.maxWait}")
+ private int maxWait;
+
+ @Value("${spring.datasource.druid.connectTimeout}")
+ private int connectTimeout;
+
+ @Value("${spring.datasource.druid.socketTimeout}")
+ private int socketTimeout;
+
+ @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
+ private int timeBetweenEvictionRunsMillis;
+
+ @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
+ private int minEvictableIdleTimeMillis;
+
+ @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
+ private int maxEvictableIdleTimeMillis;
+
+ @Value("${spring.datasource.druid.validationQuery}")
+ private String validationQuery;
+
+ @Value("${spring.datasource.druid.testWhileIdle}")
+ private boolean testWhileIdle;
+
+ @Value("${spring.datasource.druid.testOnBorrow}")
+ private boolean testOnBorrow;
+
+ @Value("${spring.datasource.druid.testOnReturn}")
+ private boolean testOnReturn;
+
+ public DruidDataSource dataSource(DruidDataSource datasource)
+ {
+ /** 配置初始化大小、最小、最大 */
+ datasource.setInitialSize(initialSize);
+ datasource.setMaxActive(maxActive);
+ datasource.setMinIdle(minIdle);
+
+ /** 配置获取连接等待超时的时间 */
+ datasource.setMaxWait(maxWait);
+
+ /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
+ datasource.setConnectTimeout(connectTimeout);
+
+ /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
+ datasource.setSocketTimeout(socketTimeout);
+
+ /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+ /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+ datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+ /**
+ * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+ */
+ datasource.setValidationQuery(validationQuery);
+ /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+ datasource.setTestWhileIdle(testWhileIdle);
+ /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+ datasource.setTestOnBorrow(testOnBorrow);
+ /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+ datasource.setTestOnReturn(testOnReturn);
+ return datasource;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java
new file mode 100644
index 0000000..277ba3b
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java
@@ -0,0 +1,73 @@
+package com.ruoyi.framework.config.properties;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.RegExUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import com.ruoyi.common.annotation.Anonymous;
+
+/**
+ * 设置Anonymous注解允许匿名访问的url
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware
+{
+ private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+ private ApplicationContext applicationContext;
+
+ private List<String> urls = new ArrayList<>();
+
+ public String ASTERISK = "*";
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
+ Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
+
+ map.keySet().forEach(info -> {
+ HandlerMethod handlerMethod = map.get(info);
+
+ // 获取方法上边的注解 替代path variable 为 *
+ Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
+ Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) //
+ .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+
+ // 获取类上边的注解, 替代path variable 为 *
+ Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
+ Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues())
+ .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+ });
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException
+ {
+ this.applicationContext = context;
+ }
+
+ public List<String> getUrls()
+ {
+ return urls;
+ }
+
+ public void setUrls(List<String> urls)
+ {
+ this.urls = urls;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java
new file mode 100644
index 0000000..e70b8cf
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java
@@ -0,0 +1,26 @@
+package com.ruoyi.framework.datasource;
+
+import java.util.Map;
+import javax.sql.DataSource;
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+/**
+ * 动态数据源
+ *
+ * @author ruoyi
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+ public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+ {
+ super.setDefaultTargetDataSource(defaultTargetDataSource);
+ super.setTargetDataSources(targetDataSources);
+ super.afterPropertiesSet();
+ }
+
+ @Override
+ protected Object determineCurrentLookupKey()
+ {
+ return DynamicDataSourceContextHolder.getDataSourceType();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java
new file mode 100644
index 0000000..9770af6
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java
@@ -0,0 +1,45 @@
+package com.ruoyi.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ *
+ * @author ruoyi
+ */
+public class DynamicDataSourceContextHolder
+{
+ public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+ /**
+ * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+ * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+ */
+ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+ /**
+ * 设置数据源的变量
+ */
+ public static void setDataSourceType(String dsType)
+ {
+ log.info("切换到{}数据源", dsType);
+ CONTEXT_HOLDER.set(dsType);
+ }
+
+ /**
+ * 获得数据源的变量
+ */
+ public static String getDataSourceType()
+ {
+ return CONTEXT_HOLDER.get();
+ }
+
+ /**
+ * 清空数据源变量
+ */
+ public static void clearDataSourceType()
+ {
+ CONTEXT_HOLDER.remove();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java
new file mode 100644
index 0000000..4e8d20f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java
@@ -0,0 +1,56 @@
+package com.ruoyi.framework.interceptor;
+
+import java.lang.reflect.Method;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.RepeatSubmit;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 防止重复提交拦截器
+ *
+ * @author ruoyi
+ */
+@Component
+public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
+{
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+ {
+ if (handler instanceof HandlerMethod)
+ {
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+ Method method = handlerMethod.getMethod();
+ RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+ if (annotation != null)
+ {
+ if (this.isRepeatSubmit(request, annotation))
+ {
+ AjaxResult ajaxResult = AjaxResult.error(annotation.message());
+ ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
+ return false;
+ }
+ }
+ return true;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ /**
+ * 验证是否重复提交由子类实现具体的防重复提交的规则
+ *
+ * @param request 请求信息
+ * @param annotation 防重复注解参数
+ * @return 结果
+ * @throws Exception
+ */
+ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
new file mode 100644
index 0000000..c93df50
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
@@ -0,0 +1,110 @@
+package com.ruoyi.framework.interceptor.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.RepeatSubmit;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.http.HttpHelper;
+import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
+ * @author ruoyi
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+ public final String REPEAT_PARAMS = "repeatParams";
+
+ public final String REPEAT_TIME = "repeatTime";
+
+ // 令牌自定义标识
+ @Value("${token.header}")
+ private String header;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
+ {
+ String nowParams = "";
+ if (request instanceof RepeatedlyRequestWrapper)
+ {
+ RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+ nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+ }
+
+ // body参数为空,获取Parameter的数据
+ if (StringUtils.isEmpty(nowParams))
+ {
+ nowParams = JSON.toJSONString(request.getParameterMap());
+ }
+ Map<String, Object> nowDataMap = new HashMap<String, Object>();
+ nowDataMap.put(REPEAT_PARAMS, nowParams);
+ nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+ // 请求地址(作为存放cache的key值)
+ String url = request.getRequestURI();
+
+ // 唯一值(没有消息头则使用请求地址)
+ String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
+
+ // 唯一标识(指定key + url + 消息头)
+ String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
+
+ Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+ if (sessionObj != null)
+ {
+ Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+ if (sessionMap.containsKey(url))
+ {
+ Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+ if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
+ {
+ return true;
+ }
+ }
+ }
+ Map<String, Object> cacheMap = new HashMap<String, Object>();
+ cacheMap.put(url, nowDataMap);
+ redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
+ return false;
+ }
+
+ /**
+ * 判断参数是否相同
+ */
+ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+ {
+ String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+ String preParams = (String) preMap.get(REPEAT_PARAMS);
+ return nowParams.equals(preParams);
+ }
+
+ /**
+ * 判断两次间隔时间
+ */
+ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
+ {
+ long time1 = (Long) nowMap.get(REPEAT_TIME);
+ long time2 = (Long) preMap.get(REPEAT_TIME);
+ if ((time1 - time2) < interval)
+ {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java
new file mode 100644
index 0000000..7387a02
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java
@@ -0,0 +1,55 @@
+package com.ruoyi.framework.manager;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import com.ruoyi.common.utils.Threads;
+import com.ruoyi.common.utils.spring.SpringUtils;
+
+/**
+ * 异步任务管理器
+ *
+ * @author ruoyi
+ */
+public class AsyncManager
+{
+ /**
+ * 操作延迟10毫秒
+ */
+ private final int OPERATE_DELAY_TIME = 10;
+
+ /**
+ * 异步操作任务调度线程池
+ */
+ private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+ /**
+ * 单例模式
+ */
+ private AsyncManager(){}
+
+ private static AsyncManager me = new AsyncManager();
+
+ public static AsyncManager me()
+ {
+ return me;
+ }
+
+ /**
+ * 执行任务
+ *
+ * @param task 任务
+ */
+ public void execute(TimerTask task)
+ {
+ executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * 停止任务线程池
+ */
+ public void shutdown()
+ {
+ Threads.shutdownAndAwaitTermination(executor);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java
new file mode 100644
index 0000000..095b865
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java
@@ -0,0 +1,39 @@
+package com.ruoyi.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import jakarta.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+ * @author ruoyi
+ */
+@Component
+public class ShutdownManager
+{
+ private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+ @PreDestroy
+ public void destroy()
+ {
+ shutdownAsyncManager();
+ }
+
+ /**
+ * 停止异步执行任务
+ */
+ private void shutdownAsyncManager()
+ {
+ try
+ {
+ logger.info("====关闭后台任务任务线程池====");
+ AsyncManager.me().shutdown();
+ }
+ catch (Exception e)
+ {
+ logger.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
new file mode 100644
index 0000000..267e305
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
@@ -0,0 +1,102 @@
+package com.ruoyi.framework.manager.factory;
+
+import java.util.TimerTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.utils.LogUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.AddressUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.SysLogininfor;
+import com.ruoyi.system.domain.SysOperLog;
+import com.ruoyi.system.service.ISysLogininforService;
+import com.ruoyi.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+
+/**
+ * 异步工厂(产生任务用)
+ *
+ * @author ruoyi
+ */
+public class AsyncFactory
+{
+ private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+ /**
+ * 记录登录信息
+ *
+ * @param username 用户名
+ * @param status 状态
+ * @param message 消息
+ * @param args 列表
+ * @return 任务task
+ */
+ public static TimerTask recordLogininfor(final String username, final String status, final String message,
+ final Object... args)
+ {
+ final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+ final String ip = IpUtils.getIpAddr();
+ return new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ String address = AddressUtils.getRealAddressByIP(ip);
+ StringBuilder s = new StringBuilder();
+ s.append(LogUtils.getBlock(ip));
+ s.append(address);
+ s.append(LogUtils.getBlock(username));
+ s.append(LogUtils.getBlock(status));
+ s.append(LogUtils.getBlock(message));
+ // 打印信息到日志
+ sys_user_logger.info(s.toString(), args);
+ // 获取客户端操作系统
+ String os = userAgent.getOperatingSystem().getName();
+ // 获取客户端浏览器
+ String browser = userAgent.getBrowser().getName();
+ // 封装对象
+ SysLogininfor logininfor = new SysLogininfor();
+ logininfor.setUserName(username);
+ logininfor.setIpaddr(ip);
+ logininfor.setLoginLocation(address);
+ logininfor.setBrowser(browser);
+ logininfor.setOs(os);
+ logininfor.setMsg(message);
+ // 日志状态
+ if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+ {
+ logininfor.setStatus(Constants.SUCCESS);
+ }
+ else if (Constants.LOGIN_FAIL.equals(status))
+ {
+ logininfor.setStatus(Constants.FAIL);
+ }
+ // 插入数据
+ SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+ }
+ };
+ }
+
+ /**
+ * 操作日志记录
+ *
+ * @param operLog 操作日志信息
+ * @return 任务task
+ */
+ public static TimerTask recordOper(final SysOperLog operLog)
+ {
+ return new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ // 远程查询操作地点
+ operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+ SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+ }
+ };
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
new file mode 100644
index 0000000..6c776ce
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
@@ -0,0 +1,28 @@
+package com.ruoyi.framework.security.context;
+
+import org.springframework.security.core.Authentication;
+
+/**
+ * 身份验证信息
+ *
+ * @author ruoyi
+ */
+public class AuthenticationContextHolder
+{
+ private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
+
+ public static Authentication getContext()
+ {
+ return contextHolder.get();
+ }
+
+ public static void setContext(Authentication context)
+ {
+ contextHolder.set(context);
+ }
+
+ public static void clearContext()
+ {
+ contextHolder.remove();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java
new file mode 100644
index 0000000..5472f3d
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java
@@ -0,0 +1,27 @@
+package com.ruoyi.framework.security.context;
+
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import com.ruoyi.common.core.text.Convert;
+
+/**
+ * 权限信息
+ *
+ * @author ruoyi
+ */
+public class PermissionContextHolder
+{
+ private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";
+
+ public static void setContext(String permission)
+ {
+ RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
+ RequestAttributes.SCOPE_REQUEST);
+ }
+
+ public static String getContext()
+ {
+ return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
+ RequestAttributes.SCOPE_REQUEST));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
new file mode 100644
index 0000000..9d2f494
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
@@ -0,0 +1,44 @@
+package com.ruoyi.framework.security.filter;
+
+import java.io.IOException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.web.service.TokenService;
+
+/**
+ * token过滤器 验证token有效性
+ *
+ * @author ruoyi
+ */
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
+{
+ @Autowired
+ private TokenService tokenService;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException
+ {
+ LoginUser loginUser = tokenService.getLoginUser(request);
+ if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
+ {
+ tokenService.verifyToken(loginUser);
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+ }
+ chain.doFilter(request, response);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java
new file mode 100644
index 0000000..e1789f8
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java
@@ -0,0 +1,34 @@
+package com.ruoyi.framework.security.handle;
+
+import java.io.IOException;
+import java.io.Serializable;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 认证失败处理类 返回未授权
+ *
+ * @author ruoyi
+ */
+@Component
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
+{
+ private static final long serialVersionUID = -8970718410437077606L;
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
+ throws IOException
+ {
+ int code = HttpStatus.UNAUTHORIZED;
+ String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
+ ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java
new file mode 100644
index 0000000..595bd3f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java
@@ -0,0 +1,53 @@
+package com.ruoyi.framework.security.handle;
+
+import java.io.IOException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.web.service.TokenService;
+
+/**
+ * 自定义退出处理类 返回成功
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
+{
+ @Autowired
+ private TokenService tokenService;
+
+ /**
+ * 退出处理
+ *
+ * @return
+ */
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
+ throws IOException, ServletException
+ {
+ LoginUser loginUser = tokenService.getLoginUser(request);
+ if (StringUtils.isNotNull(loginUser))
+ {
+ String userName = loginUser.getUsername();
+ // 删除用户缓存记录
+ tokenService.delLoginUser(loginUser.getToken());
+ // 记录用户退出日志
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
+ }
+ ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java
new file mode 100644
index 0000000..63b03da
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java
@@ -0,0 +1,240 @@
+package com.ruoyi.framework.web.domain;
+
+import java.net.UnknownHostException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import com.ruoyi.common.utils.Arith;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.web.domain.server.Cpu;
+import com.ruoyi.framework.web.domain.server.Jvm;
+import com.ruoyi.framework.web.domain.server.Mem;
+import com.ruoyi.framework.web.domain.server.Sys;
+import com.ruoyi.framework.web.domain.server.SysFile;
+import oshi.SystemInfo;
+import oshi.hardware.CentralProcessor;
+import oshi.hardware.CentralProcessor.TickType;
+import oshi.hardware.GlobalMemory;
+import oshi.hardware.HardwareAbstractionLayer;
+import oshi.software.os.FileSystem;
+import oshi.software.os.OSFileStore;
+import oshi.software.os.OperatingSystem;
+import oshi.util.Util;
+
+/**
+ * 服务器相关信息
+ *
+ * @author ruoyi
+ */
+public class Server
+{
+ private static final int OSHI_WAIT_SECOND = 1000;
+
+ /**
+ * CPU相关信息
+ */
+ private Cpu cpu = new Cpu();
+
+ /**
+ * 內存相关信息
+ */
+ private Mem mem = new Mem();
+
+ /**
+ * JVM相关信息
+ */
+ private Jvm jvm = new Jvm();
+
+ /**
+ * 服务器相关信息
+ */
+ private Sys sys = new Sys();
+
+ /**
+ * 磁盘相关信息
+ */
+ private List<SysFile> sysFiles = new LinkedList<SysFile>();
+
+ public Cpu getCpu()
+ {
+ return cpu;
+ }
+
+ public void setCpu(Cpu cpu)
+ {
+ this.cpu = cpu;
+ }
+
+ public Mem getMem()
+ {
+ return mem;
+ }
+
+ public void setMem(Mem mem)
+ {
+ this.mem = mem;
+ }
+
+ public Jvm getJvm()
+ {
+ return jvm;
+ }
+
+ public void setJvm(Jvm jvm)
+ {
+ this.jvm = jvm;
+ }
+
+ public Sys getSys()
+ {
+ return sys;
+ }
+
+ public void setSys(Sys sys)
+ {
+ this.sys = sys;
+ }
+
+ public List<SysFile> getSysFiles()
+ {
+ return sysFiles;
+ }
+
+ public void setSysFiles(List<SysFile> sysFiles)
+ {
+ this.sysFiles = sysFiles;
+ }
+
+ public void copyTo() throws Exception
+ {
+ SystemInfo si = new SystemInfo();
+ HardwareAbstractionLayer hal = si.getHardware();
+
+ setCpuInfo(hal.getProcessor());
+
+ setMemInfo(hal.getMemory());
+
+ setSysInfo();
+
+ setJvmInfo();
+
+ setSysFiles(si.getOperatingSystem());
+ }
+
+ /**
+ * 设置CPU信息
+ */
+ private void setCpuInfo(CentralProcessor processor)
+ {
+ // CPU信息
+ long[] prevTicks = processor.getSystemCpuLoadTicks();
+ Util.sleep(OSHI_WAIT_SECOND);
+ long[] ticks = processor.getSystemCpuLoadTicks();
+ long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
+ long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
+ long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
+ long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
+ long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
+ long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
+ long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
+ long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
+ long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
+ cpu.setCpuNum(processor.getLogicalProcessorCount());
+ cpu.setTotal(totalCpu);
+ cpu.setSys(cSys);
+ cpu.setUsed(user);
+ cpu.setWait(iowait);
+ cpu.setFree(idle);
+ }
+
+ /**
+ * 设置内存信息
+ */
+ private void setMemInfo(GlobalMemory memory)
+ {
+ mem.setTotal(memory.getTotal());
+ mem.setUsed(memory.getTotal() - memory.getAvailable());
+ mem.setFree(memory.getAvailable());
+ }
+
+ /**
+ * 设置服务器信息
+ */
+ private void setSysInfo()
+ {
+ Properties props = System.getProperties();
+ sys.setComputerName(IpUtils.getHostName());
+ sys.setComputerIp(IpUtils.getHostIp());
+ sys.setOsName(props.getProperty("os.name"));
+ sys.setOsArch(props.getProperty("os.arch"));
+ sys.setUserDir(props.getProperty("user.dir"));
+ }
+
+ /**
+ * 设置Java虚拟机
+ */
+ private void setJvmInfo() throws UnknownHostException
+ {
+ Properties props = System.getProperties();
+ jvm.setTotal(Runtime.getRuntime().totalMemory());
+ jvm.setMax(Runtime.getRuntime().maxMemory());
+ jvm.setFree(Runtime.getRuntime().freeMemory());
+ jvm.setVersion(props.getProperty("java.version"));
+ jvm.setHome(props.getProperty("java.home"));
+ }
+
+ /**
+ * 设置磁盘信息
+ */
+ private void setSysFiles(OperatingSystem os)
+ {
+ FileSystem fileSystem = os.getFileSystem();
+ List<OSFileStore> fsArray = fileSystem.getFileStores();
+ for (OSFileStore fs : fsArray)
+ {
+ long free = fs.getUsableSpace();
+ long total = fs.getTotalSpace();
+ long used = total - free;
+ SysFile sysFile = new SysFile();
+ sysFile.setDirName(fs.getMount());
+ sysFile.setSysTypeName(fs.getType());
+ sysFile.setTypeName(fs.getName());
+ sysFile.setTotal(convertFileSize(total));
+ sysFile.setFree(convertFileSize(free));
+ sysFile.setUsed(convertFileSize(used));
+ sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100));
+ sysFiles.add(sysFile);
+ }
+ }
+
+ /**
+ * 字节转换
+ *
+ * @param size 字节大小
+ * @return 转换后值
+ */
+ public String convertFileSize(long size)
+ {
+ long kb = 1024;
+ long mb = kb * 1024;
+ long gb = mb * 1024;
+ if (size >= gb)
+ {
+ return String.format("%.1f GB", (float) size / gb);
+ }
+ else if (size >= mb)
+ {
+ float f = (float) size / mb;
+ return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
+ }
+ else if (size >= kb)
+ {
+ float f = (float) size / kb;
+ return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
+ }
+ else
+ {
+ return String.format("%d B", size);
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
new file mode 100644
index 0000000..a13a66c
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
@@ -0,0 +1,101 @@
+package com.ruoyi.framework.web.domain.server;
+
+import com.ruoyi.common.utils.Arith;
+
+/**
+ * CPU相关信息
+ *
+ * @author ruoyi
+ */
+public class Cpu
+{
+ /**
+ * 核心数
+ */
+ private int cpuNum;
+
+ /**
+ * CPU总的使用率
+ */
+ private double total;
+
+ /**
+ * CPU系统使用率
+ */
+ private double sys;
+
+ /**
+ * CPU用户使用率
+ */
+ private double used;
+
+ /**
+ * CPU当前等待率
+ */
+ private double wait;
+
+ /**
+ * CPU当前空闲率
+ */
+ private double free;
+
+ public int getCpuNum()
+ {
+ return cpuNum;
+ }
+
+ public void setCpuNum(int cpuNum)
+ {
+ this.cpuNum = cpuNum;
+ }
+
+ public double getTotal()
+ {
+ return Arith.round(Arith.mul(total, 100), 2);
+ }
+
+ public void setTotal(double total)
+ {
+ this.total = total;
+ }
+
+ public double getSys()
+ {
+ return Arith.round(Arith.mul(sys / total, 100), 2);
+ }
+
+ public void setSys(double sys)
+ {
+ this.sys = sys;
+ }
+
+ public double getUsed()
+ {
+ return Arith.round(Arith.mul(used / total, 100), 2);
+ }
+
+ public void setUsed(double used)
+ {
+ this.used = used;
+ }
+
+ public double getWait()
+ {
+ return Arith.round(Arith.mul(wait / total, 100), 2);
+ }
+
+ public void setWait(double wait)
+ {
+ this.wait = wait;
+ }
+
+ public double getFree()
+ {
+ return Arith.round(Arith.mul(free / total, 100), 2);
+ }
+
+ public void setFree(double free)
+ {
+ this.free = free;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java
new file mode 100644
index 0000000..1fdc6ac
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java
@@ -0,0 +1,130 @@
+package com.ruoyi.framework.web.domain.server;
+
+import java.lang.management.ManagementFactory;
+import com.ruoyi.common.utils.Arith;
+import com.ruoyi.common.utils.DateUtils;
+
+/**
+ * JVM相关信息
+ *
+ * @author ruoyi
+ */
+public class Jvm
+{
+ /**
+ * 当前JVM占用的内存总数(M)
+ */
+ private double total;
+
+ /**
+ * JVM最大可用内存总数(M)
+ */
+ private double max;
+
+ /**
+ * JVM空闲内存(M)
+ */
+ private double free;
+
+ /**
+ * JDK版本
+ */
+ private String version;
+
+ /**
+ * JDK路径
+ */
+ private String home;
+
+ public double getTotal()
+ {
+ return Arith.div(total, (1024 * 1024), 2);
+ }
+
+ public void setTotal(double total)
+ {
+ this.total = total;
+ }
+
+ public double getMax()
+ {
+ return Arith.div(max, (1024 * 1024), 2);
+ }
+
+ public void setMax(double max)
+ {
+ this.max = max;
+ }
+
+ public double getFree()
+ {
+ return Arith.div(free, (1024 * 1024), 2);
+ }
+
+ public void setFree(double free)
+ {
+ this.free = free;
+ }
+
+ public double getUsed()
+ {
+ return Arith.div(total - free, (1024 * 1024), 2);
+ }
+
+ public double getUsage()
+ {
+ return Arith.mul(Arith.div(total - free, total, 4), 100);
+ }
+
+ /**
+ * 获取JDK名称
+ */
+ public String getName()
+ {
+ return ManagementFactory.getRuntimeMXBean().getVmName();
+ }
+
+ public String getVersion()
+ {
+ return version;
+ }
+
+ public void setVersion(String version)
+ {
+ this.version = version;
+ }
+
+ public String getHome()
+ {
+ return home;
+ }
+
+ public void setHome(String home)
+ {
+ this.home = home;
+ }
+
+ /**
+ * JDK启动时间
+ */
+ public String getStartTime()
+ {
+ return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate());
+ }
+
+ /**
+ * JDK运行时间
+ */
+ public String getRunTime()
+ {
+ return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate());
+ }
+
+ /**
+ * 运行参数
+ */
+ public String getInputArgs()
+ {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments().toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java
new file mode 100644
index 0000000..13eec52
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java
@@ -0,0 +1,61 @@
+package com.ruoyi.framework.web.domain.server;
+
+import com.ruoyi.common.utils.Arith;
+
+/**
+ * 內存相关信息
+ *
+ * @author ruoyi
+ */
+public class Mem
+{
+ /**
+ * 内存总量
+ */
+ private double total;
+
+ /**
+ * 已用内存
+ */
+ private double used;
+
+ /**
+ * 剩余内存
+ */
+ private double free;
+
+ public double getTotal()
+ {
+ return Arith.div(total, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setTotal(long total)
+ {
+ this.total = total;
+ }
+
+ public double getUsed()
+ {
+ return Arith.div(used, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setUsed(long used)
+ {
+ this.used = used;
+ }
+
+ public double getFree()
+ {
+ return Arith.div(free, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setFree(long free)
+ {
+ this.free = free;
+ }
+
+ public double getUsage()
+ {
+ return Arith.mul(Arith.div(used, total, 4), 100);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java
new file mode 100644
index 0000000..45d64d9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java
@@ -0,0 +1,84 @@
+package com.ruoyi.framework.web.domain.server;
+
+/**
+ * 系统相关信息
+ *
+ * @author ruoyi
+ */
+public class Sys
+{
+ /**
+ * 服务器名称
+ */
+ private String computerName;
+
+ /**
+ * 服务器Ip
+ */
+ private String computerIp;
+
+ /**
+ * 项目路径
+ */
+ private String userDir;
+
+ /**
+ * 操作系统
+ */
+ private String osName;
+
+ /**
+ * 系统架构
+ */
+ private String osArch;
+
+ public String getComputerName()
+ {
+ return computerName;
+ }
+
+ public void setComputerName(String computerName)
+ {
+ this.computerName = computerName;
+ }
+
+ public String getComputerIp()
+ {
+ return computerIp;
+ }
+
+ public void setComputerIp(String computerIp)
+ {
+ this.computerIp = computerIp;
+ }
+
+ public String getUserDir()
+ {
+ return userDir;
+ }
+
+ public void setUserDir(String userDir)
+ {
+ this.userDir = userDir;
+ }
+
+ public String getOsName()
+ {
+ return osName;
+ }
+
+ public void setOsName(String osName)
+ {
+ this.osName = osName;
+ }
+
+ public String getOsArch()
+ {
+ return osArch;
+ }
+
+ public void setOsArch(String osArch)
+ {
+ this.osArch = osArch;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java
new file mode 100644
index 0000000..1320cde
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java
@@ -0,0 +1,114 @@
+package com.ruoyi.framework.web.domain.server;
+
+/**
+ * 系统文件相关信息
+ *
+ * @author ruoyi
+ */
+public class SysFile
+{
+ /**
+ * 盘符路径
+ */
+ private String dirName;
+
+ /**
+ * 盘符类型
+ */
+ private String sysTypeName;
+
+ /**
+ * 文件类型
+ */
+ private String typeName;
+
+ /**
+ * 总大小
+ */
+ private String total;
+
+ /**
+ * 剩余大小
+ */
+ private String free;
+
+ /**
+ * 已经使用量
+ */
+ private String used;
+
+ /**
+ * 资源的使用率
+ */
+ private double usage;
+
+ public String getDirName()
+ {
+ return dirName;
+ }
+
+ public void setDirName(String dirName)
+ {
+ this.dirName = dirName;
+ }
+
+ public String getSysTypeName()
+ {
+ return sysTypeName;
+ }
+
+ public void setSysTypeName(String sysTypeName)
+ {
+ this.sysTypeName = sysTypeName;
+ }
+
+ public String getTypeName()
+ {
+ return typeName;
+ }
+
+ public void setTypeName(String typeName)
+ {
+ this.typeName = typeName;
+ }
+
+ public String getTotal()
+ {
+ return total;
+ }
+
+ public void setTotal(String total)
+ {
+ this.total = total;
+ }
+
+ public String getFree()
+ {
+ return free;
+ }
+
+ public void setFree(String free)
+ {
+ this.free = free;
+ }
+
+ public String getUsed()
+ {
+ return used;
+ }
+
+ public void setUsed(String used)
+ {
+ this.used = used;
+ }
+
+ public double getUsage()
+ {
+ return usage;
+ }
+
+ public void setUsage(double usage)
+ {
+ this.usage = usage;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..1ca0283
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
@@ -0,0 +1,145 @@
+package com.ruoyi.framework.web.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.exception.DemoModeException;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.html.EscapeUtil;
+
+/**
+ * 全局异常处理器
+ *
+ * @author ruoyi
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler
+{
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ /**
+ * 权限校验异常
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
+ return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
+ }
+
+ /**
+ * 请求方式不支持
+ */
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
+ HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 业务异常
+ */
+ @ExceptionHandler(ServiceException.class)
+ public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request)
+ {
+ log.error(e.getMessage(), e);
+ Integer code = e.getCode();
+ return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 请求路径中缺少必需的路径变量
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+ }
+
+ /**
+ * 请求参数类型不匹配
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ String value = Convert.toStr(e.getValue());
+ if (StringUtils.isNotEmpty(value))
+ {
+ value = EscapeUtil.clean(value);
+ }
+ log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
+ }
+
+ /**
+ * 拦截未知的运行时异常
+ */
+ @ExceptionHandler(RuntimeException.class)
+ public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生未知异常.", requestURI, e);
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 系统异常
+ */
+ @ExceptionHandler(Exception.class)
+ public AjaxResult handleException(Exception e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(BindException.class)
+ public AjaxResult handleBindException(BindException e)
+ {
+ log.error(e.getMessage(), e);
+ String message = e.getAllErrors().get(0).getDefaultMessage();
+ return AjaxResult.error(message);
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
+ {
+ log.error(e.getMessage(), e);
+ String message = e.getBindingResult().getFieldError().getDefaultMessage();
+ return AjaxResult.error(message);
+ }
+
+ /**
+ * 演示模式异常
+ */
+ @ExceptionHandler(DemoModeException.class)
+ public AjaxResult handleDemoModeException(DemoModeException e)
+ {
+ return AjaxResult.error("演示模式,不允许操作");
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java
new file mode 100644
index 0000000..07d259a
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java
@@ -0,0 +1,159 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.Set;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.context.PermissionContextHolder;
+
+/**
+ * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
+ *
+ * @author ruoyi
+ */
+@Service("ss")
+public class PermissionService
+{
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ public boolean hasPermi(String permission)
+ {
+ if (StringUtils.isEmpty(permission))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ PermissionContextHolder.setContext(permission);
+ return hasPermissions(loginUser.getPermissions(), permission);
+ }
+
+ /**
+ * 验证用户是否不具备某权限,与 hasPermi逻辑相反
+ *
+ * @param permission 权限字符串
+ * @return 用户是否不具备某权限
+ */
+ public boolean lacksPermi(String permission)
+ {
+ return hasPermi(permission) != true;
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个权限
+ *
+ * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表
+ * @return 用户是否具有以下任意一个权限
+ */
+ public boolean hasAnyPermi(String permissions)
+ {
+ if (StringUtils.isEmpty(permissions))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ PermissionContextHolder.setContext(permissions);
+ Set<String> authorities = loginUser.getPermissions();
+ for (String permission : permissions.split(Constants.PERMISSION_DELIMETER))
+ {
+ if (permission != null && hasPermissions(authorities, permission))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断用户是否拥有某个角色
+ *
+ * @param role 角色字符串
+ * @return 用户是否具备某角色
+ */
+ public boolean hasRole(String role)
+ {
+ if (StringUtils.isEmpty(role))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+ {
+ return false;
+ }
+ for (SysRole sysRole : loginUser.getUser().getRoles())
+ {
+ String roleKey = sysRole.getRoleKey();
+ if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 验证用户是否不具备某角色,与 isRole逻辑相反。
+ *
+ * @param role 角色名称
+ * @return 用户是否不具备某角色
+ */
+ public boolean lacksRole(String role)
+ {
+ return hasRole(role) != true;
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个角色
+ *
+ * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
+ * @return 用户是否具有以下任意一个角色
+ */
+ public boolean hasAnyRoles(String roles)
+ {
+ if (StringUtils.isEmpty(roles))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+ {
+ return false;
+ }
+ for (String role : roles.split(Constants.ROLE_DELIMETER))
+ {
+ if (hasRole(role))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断是否包含权限
+ *
+ * @param permissions 权限列表
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ private boolean hasPermissions(Set<String> permissions, String permission)
+ {
+ return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
new file mode 100644
index 0000000..5575e77
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
@@ -0,0 +1,188 @@
+package com.ruoyi.framework.web.service;
+
+import jakarta.annotation.Resource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.exception.user.BlackListException;
+import com.ruoyi.common.exception.user.CaptchaException;
+import com.ruoyi.common.exception.user.CaptchaExpireException;
+import com.ruoyi.common.exception.user.UserNotExistsException;
+import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 登录校验方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysLoginService
+{
+ @Autowired
+ private TokenService tokenService;
+
+ @Resource
+ private AuthenticationManager authenticationManager;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysConfigService configService;
+
+
+
+ /**
+ * 登录验证
+ *
+ * @param username 用户名
+ * @param password 密码
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public String login(String username, String password, String code, String uuid)
+ {
+
+ if (!(code.equals("jrx") && uuid.equals("jrx"))){
+ // 验证码校验
+ validateCaptcha(username, code, uuid);
+ }
+ // 登录前置校验
+ loginPreCheck(username, password);
+ // 用户验证
+ Authentication authentication = null;
+
+ try
+ {
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
+ AuthenticationContextHolder.setContext(authenticationToken);
+ // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+ authentication = authenticationManager.authenticate(authenticationToken);
+ }
+ catch (Exception e)
+ {
+ if (e instanceof BadCredentialsException)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ else
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+ throw new ServiceException(e.getMessage());
+ }
+ }
+ finally
+ {
+ AuthenticationContextHolder.clearContext();
+ }
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+ LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+ recordLoginInfo(loginUser.getUserId());
+ // 生成token
+ return tokenService.createToken(loginUser);
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public void validateCaptcha(String username, String code, String uuid)
+ {
+ boolean captchaEnabled = configService.selectCaptchaEnabled();
+ if (captchaEnabled)
+ {
+ String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
+ String captcha = redisCache.getCacheObject(verifyKey);
+ if (captcha == null)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+ throw new CaptchaExpireException();
+ }
+ redisCache.deleteObject(verifyKey);
+ if (!code.equalsIgnoreCase(captcha))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+ throw new CaptchaException();
+ }
+ }
+ }
+
+ /**
+ * 登录前置校验
+ * @param username 用户名
+ * @param password 用户密码
+ */
+ public void loginPreCheck(String username, String password)
+ {
+ // 用户名或密码为空 错误
+ if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
+ throw new UserNotExistsException();
+ }
+ // 密码如果不在指定范围内 错误
+ if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
+ || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ // 用户名不在指定范围内 错误
+ if (username.length() < UserConstants.USERNAME_MIN_LENGTH
+ || username.length() > UserConstants.USERNAME_MAX_LENGTH)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ // IP黑名单校验
+ String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
+ if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
+ throw new BlackListException();
+ }
+ }
+
+ /**
+ * 记录登录信息
+ *
+ * @param userId 用户ID
+ */
+ public void recordLoginInfo(Long userId)
+ {
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(userId);
+ sysUser.setLoginIp(IpUtils.getIpAddr());
+ sysUser.setLoginDate(DateUtils.getNowDate());
+ userService.updateUserProfile(sysUser);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
new file mode 100644
index 0000000..6728c7b
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
@@ -0,0 +1,86 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
+import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
+
+/**
+ * 登录密码方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysPasswordService
+{
+ @Autowired
+ private RedisCache redisCache;
+
+ @Value(value = "${user.password.maxRetryCount}")
+ private int maxRetryCount;
+
+ @Value(value = "${user.password.lockTime}")
+ private int lockTime;
+
+ /**
+ * 登录账户密码错误次数缓存键名
+ *
+ * @param username 用户名
+ * @return 缓存键key
+ */
+ private String getCacheKey(String username)
+ {
+ return CacheConstants.PWD_ERR_CNT_KEY + username;
+ }
+
+ public void validate(SysUser user)
+ {
+ Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
+ String username = usernamePasswordAuthenticationToken.getName();
+ String password = usernamePasswordAuthenticationToken.getCredentials().toString();
+
+ Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
+
+ if (retryCount == null)
+ {
+ retryCount = 0;
+ }
+
+ if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
+ {
+ throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
+ }
+
+ if (!matches(user, password))
+ {
+ retryCount = retryCount + 1;
+ redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
+ throw new UserPasswordNotMatchException();
+ }
+ else
+ {
+ clearLoginRecordCache(username);
+ }
+ }
+
+ public boolean matches(SysUser user, String rawPassword)
+ {
+ return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
+ }
+
+ public void clearLoginRecordCache(String loginName)
+ {
+ if (redisCache.hasKey(getCacheKey(loginName)))
+ {
+ redisCache.deleteObject(getCacheKey(loginName));
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java
new file mode 100644
index 0000000..51c4dd9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java
@@ -0,0 +1,88 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysMenuService;
+import com.ruoyi.system.service.ISysRoleService;
+
+/**
+ * 用户权限处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysPermissionService
+{
+ @Autowired
+ private ISysRoleService roleService;
+
+ @Autowired
+ private ISysMenuService menuService;
+
+ /**
+ * 获取角色数据权限
+ *
+ * @param user 用户信息
+ * @return 角色权限信息
+ */
+ public Set<String> getRolePermission(SysUser user)
+ {
+ Set<String> roles = new HashSet<String>();
+ // 管理员拥有所有权限
+ if (user.isAdmin())
+ {
+ roles.add("admin");
+ }
+ else
+ {
+ roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
+ }
+ return roles;
+ }
+
+ /**
+ * 获取菜单数据权限
+ *
+ * @param user 用户信息
+ * @return 菜单权限信息
+ */
+ public Set<String> getMenuPermission(SysUser user)
+ {
+ Set<String> perms = new HashSet<String>();
+ // 管理员拥有所有权限
+ if (user.isAdmin())
+ {
+ perms.add("*:*:*");
+ }
+ else
+ {
+ List<SysRole> roles = user.getRoles();
+ if (!CollectionUtils.isEmpty(roles))
+ {
+ // 多角色设置permissions属性,以便数据权限匹配权限
+ for (SysRole role : roles)
+ {
+ if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL))
+ {
+ Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
+ role.setPermissions(rolePerms);
+ perms.addAll(rolePerms);
+ }
+ }
+ }
+ else
+ {
+ perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
+ }
+ }
+ return perms;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
new file mode 100644
index 0000000..f2afe31
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
@@ -0,0 +1,115 @@
+package com.ruoyi.framework.web.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.RegisterBody;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.user.CaptchaException;
+import com.ruoyi.common.exception.user.CaptchaExpireException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 注册校验方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysRegisterService
+{
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ /**
+ * 注册
+ */
+ public String register(RegisterBody registerBody)
+ {
+ String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword();
+ SysUser sysUser = new SysUser();
+ sysUser.setUserName(username);
+
+ // 验证码开关
+ boolean captchaEnabled = configService.selectCaptchaEnabled();
+ if (captchaEnabled)
+ {
+ validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
+ }
+
+ if (StringUtils.isEmpty(username))
+ {
+ msg = "用户名不能为空";
+ }
+ else if (StringUtils.isEmpty(password))
+ {
+ msg = "用户密码不能为空";
+ }
+ else if (username.length() < UserConstants.USERNAME_MIN_LENGTH
+ || username.length() > UserConstants.USERNAME_MAX_LENGTH)
+ {
+ msg = "账户长度必须在2到20个字符之间";
+ }
+ else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
+ || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
+ {
+ msg = "密码长度必须在5到20个字符之间";
+ }
+ else if (!userService.checkUserNameUnique(sysUser))
+ {
+ msg = "保存用户'" + username + "'失败,注册账号已存在";
+ }
+ else
+ {
+ sysUser.setNickName(username);
+ sysUser.setPassword(SecurityUtils.encryptPassword(password));
+ boolean regFlag = userService.registerUser(sysUser);
+ if (!regFlag)
+ {
+ msg = "注册失败,请联系系统管理人员";
+ }
+ else
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success")));
+ }
+ }
+ return msg;
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public void validateCaptcha(String username, String code, String uuid)
+ {
+ String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
+ String captcha = redisCache.getCacheObject(verifyKey);
+ redisCache.deleteObject(verifyKey);
+ if (captcha == null)
+ {
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha))
+ {
+ throw new CaptchaException();
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
new file mode 100644
index 0000000..a03ebad
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
@@ -0,0 +1,231 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.AddressUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import eu.bitwalker.useragentutils.UserAgent;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+/**
+ * token验证处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class TokenService
+{
+ private static final Logger log = LoggerFactory.getLogger(TokenService.class);
+
+ // 令牌自定义标识
+ @Value("${token.header}")
+ private String header;
+
+ // 令牌秘钥
+ @Value("${token.secret}")
+ private String secret;
+
+ // 令牌有效期(默认30分钟)
+ @Value("${token.expireTime}")
+ private int expireTime;
+
+ protected static final long MILLIS_SECOND = 1000;
+
+ protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+
+ private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ /**
+ * 获取用户身份信息
+ *
+ * @return 用户信息
+ */
+ public LoginUser getLoginUser(HttpServletRequest request)
+ {
+ // 获取请求携带的令牌
+ String token = getToken(request);
+ if (StringUtils.isNotEmpty(token))
+ {
+ try
+ {
+ Claims claims = parseToken(token);
+ // 解析对应的权限以及用户信息
+ String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
+ String userKey = getTokenKey(uuid);
+ LoginUser user = redisCache.getCacheObject(userKey);
+ return user;
+ }
+ catch (Exception e)
+ {
+ log.error("获取用户信息异常'{}'", e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 设置用户身份信息
+ */
+ public void setLoginUser(LoginUser loginUser)
+ {
+ if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
+ {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 删除用户身份信息
+ */
+ public void delLoginUser(String token)
+ {
+ if (StringUtils.isNotEmpty(token))
+ {
+ String userKey = getTokenKey(token);
+ redisCache.deleteObject(userKey);
+ }
+ }
+
+ /**
+ * 创建令牌
+ *
+ * @param loginUser 用户信息
+ * @return 令牌
+ */
+ public String createToken(LoginUser loginUser)
+ {
+ String token = IdUtils.fastUUID();
+ loginUser.setToken(token);
+ setUserAgent(loginUser);
+ refreshToken(loginUser);
+
+ Map<String, Object> claims = new HashMap<>();
+ claims.put(Constants.LOGIN_USER_KEY, token);
+ return createToken(claims);
+ }
+
+ /**
+ * 验证令牌有效期,相差不足20分钟,自动刷新缓存
+ *
+ * @param loginUser
+ * @return 令牌
+ */
+ public void verifyToken(LoginUser loginUser)
+ {
+ long expireTime = loginUser.getExpireTime();
+ long currentTime = System.currentTimeMillis();
+ if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
+ {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 刷新令牌有效期
+ *
+ * @param loginUser 登录信息
+ */
+ public void refreshToken(LoginUser loginUser)
+ {
+ loginUser.setLoginTime(System.currentTimeMillis());
+ loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
+ // 根据uuid将loginUser缓存
+ String userKey = getTokenKey(loginUser.getToken());
+ redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+ }
+
+ /**
+ * 设置用户代理信息
+ *
+ * @param loginUser 登录信息
+ */
+ public void setUserAgent(LoginUser loginUser)
+ {
+ UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+ String ip = IpUtils.getIpAddr();
+ loginUser.setIpaddr(ip);
+ loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+ loginUser.setBrowser(userAgent.getBrowser().getName());
+ loginUser.setOs(userAgent.getOperatingSystem().getName());
+ }
+
+ /**
+ * 从数据声明生成令牌
+ *
+ * @param claims 数据声明
+ * @return 令牌
+ */
+ private String createToken(Map<String, Object> claims)
+ {
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .signWith(SignatureAlgorithm.HS512, secret).compact();
+ return token;
+ }
+
+ /**
+ * 从令牌中获取数据声明
+ *
+ * @param token 令牌
+ * @return 数据声明
+ */
+ private Claims parseToken(String token)
+ {
+ return Jwts.parser()
+ .setSigningKey(secret)
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ /**
+ * 从令牌中获取用户名
+ *
+ * @param token 令牌
+ * @return 用户名
+ */
+ public String getUsernameFromToken(String token)
+ {
+ Claims claims = parseToken(token);
+ return claims.getSubject();
+ }
+
+ /**
+ * 获取请求token
+ *
+ * @param request
+ * @return token
+ */
+ private String getToken(HttpServletRequest request)
+ {
+ String token = request.getHeader(header);
+ if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
+ {
+ token = token.replace(Constants.TOKEN_PREFIX, "");
+ }
+ return token;
+ }
+
+ private String getTokenKey(String uuid)
+ {
+ return CacheConstants.LOGIN_TOKEN_KEY + uuid;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..5dcdf90
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
@@ -0,0 +1,66 @@
+package com.ruoyi.framework.web.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 用户验证处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService
+{
+ private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private SysPasswordService passwordService;
+
+ @Autowired
+ private SysPermissionService permissionService;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
+ {
+ SysUser user = userService.selectUserByUserName(username);
+ if (StringUtils.isNull(user))
+ {
+ log.info("登录用户:{} 不存在.", username);
+ throw new ServiceException(MessageUtils.message("user.not.exists"));
+ }
+ else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
+ {
+ log.info("登录用户:{} 已被删除.", username);
+ throw new ServiceException(MessageUtils.message("user.password.delete"));
+ }
+ else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
+ {
+ log.info("登录用户:{} 已被停用.", username);
+ throw new ServiceException(MessageUtils.message("user.blocked"));
+ }
+
+ passwordService.validate(user);
+
+ return createLoginUser(user);
+ }
+
+ public UserDetails createLoginUser(SysUser user)
+ {
+ return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
+ }
+}
diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml
new file mode 100644
index 0000000..69ab1d9
--- /dev/null
+++ b/ruoyi-generator/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-generator</artifactId>
+
+ <description>
+ generator代码生成
+ </description>
+
+ <dependencies>
+
+ <!-- velocity代码生成使用模板 -->
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity-engine-core</artifactId>
+ </dependency>
+
+ <!-- 通用工具-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ </dependency>
+
+ <!-- 阿里数据库连接池 -->
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>druid-spring-boot-3-starter</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>6.0.0</version>
+ <scope>compile</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java
new file mode 100644
index 0000000..cc4cd14
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java
@@ -0,0 +1,73 @@
+package com.ruoyi.generator.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取代码生成相关配置
+ *
+ * @author ruoyi
+ */
+@Component
+@ConfigurationProperties(prefix = "gen")
+@PropertySource(value = { "classpath:generator.yml" })
+public class GenConfig
+{
+ /** 作者 */
+ public static String author;
+
+ /** 生成包路径 */
+ public static String packageName;
+
+ /** 自动去除表前缀,默认是false */
+ public static boolean autoRemovePre;
+
+ /** 表前缀(类名不会包含表前缀) */
+ public static String tablePrefix;
+
+ public static String getAuthor()
+ {
+ return author;
+ }
+
+ @Value("${author}")
+ public void setAuthor(String author)
+ {
+ GenConfig.author = author;
+ }
+
+ public static String getPackageName()
+ {
+ return packageName;
+ }
+
+ @Value("${packageName}")
+ public void setPackageName(String packageName)
+ {
+ GenConfig.packageName = packageName;
+ }
+
+ public static boolean getAutoRemovePre()
+ {
+ return autoRemovePre;
+ }
+
+ @Value("${autoRemovePre}")
+ public void setAutoRemovePre(boolean autoRemovePre)
+ {
+ GenConfig.autoRemovePre = autoRemovePre;
+ }
+
+ public static String getTablePrefix()
+ {
+ return tablePrefix;
+ }
+
+ @Value("${tablePrefix}")
+ public void setTablePrefix(String tablePrefix)
+ {
+ GenConfig.tablePrefix = tablePrefix;
+ }
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java
new file mode 100644
index 0000000..5f5c75e
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java
@@ -0,0 +1,258 @@
+package com.ruoyi.generator.controller;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.alibaba.druid.DbType;
+import com.alibaba.druid.sql.SQLUtils;
+import com.alibaba.druid.sql.ast.SQLStatement;
+import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.sql.SqlUtil;
+import com.ruoyi.generator.domain.GenTable;
+import com.ruoyi.generator.domain.GenTableColumn;
+import com.ruoyi.generator.service.IGenTableColumnService;
+import com.ruoyi.generator.service.IGenTableService;
+
+/**
+ * 代码生成 操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/tool/gen")
+public class GenController extends BaseController
+{
+ @Autowired
+ private IGenTableService genTableService;
+
+ @Autowired
+ private IGenTableColumnService genTableColumnService;
+
+ /**
+ * 查询代码生成列表
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:list')")
+ @GetMapping("/list")
+ public TableDataInfo genList(GenTable genTable)
+ {
+ startPage();
+ List<GenTable> list = genTableService.selectGenTableList(genTable);
+ return getDataTable(list);
+ }
+
+ /**
+ * 修改代码生成业务
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:query')")
+ @GetMapping(value = "/{tableId}")
+ public AjaxResult getInfo(@PathVariable Long tableId)
+ {
+ GenTable table = genTableService.selectGenTableById(tableId);
+ List<GenTable> tables = genTableService.selectGenTableAll();
+ List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(tableId);
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("info", table);
+ map.put("rows", list);
+ map.put("tables", tables);
+ return success(map);
+ }
+
+ /**
+ * 查询数据库列表
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:list')")
+ @GetMapping("/db/list")
+ public TableDataInfo dataList(GenTable genTable)
+ {
+ startPage();
+ List<GenTable> list = genTableService.selectDbTableList(genTable);
+ return getDataTable(list);
+ }
+
+ /**
+ * 查询数据表字段列表
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:list')")
+ @GetMapping(value = "/column/{tableId}")
+ public TableDataInfo columnList(Long tableId)
+ {
+ TableDataInfo dataInfo = new TableDataInfo();
+ List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(tableId);
+ dataInfo.setRows(list);
+ dataInfo.setTotal(list.size());
+ return dataInfo;
+ }
+
+ /**
+ * 导入表结构(保存)
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:import')")
+ @Log(title = "代码生成", businessType = BusinessType.IMPORT)
+ @PostMapping("/importTable")
+ public AjaxResult importTableSave(String tables)
+ {
+ String[] tableNames = Convert.toStrArray(tables);
+ // 查询表信息
+ List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
+ genTableService.importGenTable(tableList, SecurityUtils.getUsername());
+ return success();
+ }
+
+ /**
+ * 创建表结构(保存)
+ */
+ @PreAuthorize("@ss.hasRole('admin')")
+ @Log(title = "创建表", businessType = BusinessType.OTHER)
+ @PostMapping("/createTable")
+ public AjaxResult createTableSave(String sql)
+ {
+ try
+ {
+ SqlUtil.filterKeyword(sql);
+ List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql);
+ List<String> tableNames = new ArrayList<>();
+ for (SQLStatement sqlStatement : sqlStatements)
+ {
+ if (sqlStatement instanceof MySqlCreateTableStatement)
+ {
+ MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement;
+ if (genTableService.createTable(createTableStatement.toString()))
+ {
+ String tableName = createTableStatement.getTableName().replaceAll("`", "");
+ tableNames.add(tableName);
+ }
+ }
+ }
+ List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()]));
+ String operName = SecurityUtils.getUsername();
+ genTableService.importGenTable(tableList, operName);
+ return AjaxResult.success();
+ }
+ catch (Exception e)
+ {
+ logger.error(e.getMessage(), e);
+ return AjaxResult.error("创建表结构异常");
+ }
+ }
+
+ /**
+ * 修改保存代码生成业务
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
+ @Log(title = "代码生成", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult editSave(@Validated @RequestBody GenTable genTable)
+ {
+ genTableService.validateEdit(genTable);
+ genTableService.updateGenTable(genTable);
+ return success();
+ }
+
+ /**
+ * 删除代码生成
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:remove')")
+ @Log(title = "代码生成", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{tableIds}")
+ public AjaxResult remove(@PathVariable Long[] tableIds)
+ {
+ genTableService.deleteGenTableByIds(tableIds);
+ return success();
+ }
+
+ /**
+ * 预览代码
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:preview')")
+ @GetMapping("/preview/{tableId}")
+ public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException
+ {
+ Map<String, String> dataMap = genTableService.previewCode(tableId);
+ return success(dataMap);
+ }
+
+ /**
+ * 生成代码(下载方式)
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:code')")
+ @Log(title = "代码生成", businessType = BusinessType.GENCODE)
+ @GetMapping("/download/{tableName}")
+ public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException
+ {
+ byte[] data = genTableService.downloadCode(tableName);
+ genCode(response, data);
+ }
+
+ /**
+ * 生成代码(自定义路径)
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:code')")
+ @Log(title = "代码生成", businessType = BusinessType.GENCODE)
+ @GetMapping("/genCode/{tableName}")
+ public AjaxResult genCode(@PathVariable("tableName") String tableName)
+ {
+ genTableService.generatorCode(tableName);
+ return success();
+ }
+
+ /**
+ * 同步数据库
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
+ @Log(title = "代码生成", businessType = BusinessType.UPDATE)
+ @GetMapping("/synchDb/{tableName}")
+ public AjaxResult synchDb(@PathVariable("tableName") String tableName)
+ {
+ genTableService.synchDb(tableName);
+ return success();
+ }
+
+ /**
+ * 批量生成代码
+ */
+ @PreAuthorize("@ss.hasPermi('tool:gen:code')")
+ @Log(title = "代码生成", businessType = BusinessType.GENCODE)
+ @GetMapping("/batchGenCode")
+ public void batchGenCode(HttpServletResponse response, String tables) throws IOException
+ {
+ String[] tableNames = Convert.toStrArray(tables);
+ byte[] data = genTableService.downloadCode(tableNames);
+ genCode(response, data);
+ }
+
+ /**
+ * 生成zip文件
+ */
+ private void genCode(HttpServletResponse response, byte[] data) throws IOException
+ {
+ response.reset();
+ response.addHeader("Access-Control-Allow-Origin", "*");
+ response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
+ response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\"");
+ response.addHeader("Content-Length", "" + data.length);
+ response.setContentType("application/octet-stream; charset=UTF-8");
+ IOUtils.write(data, response.getOutputStream());
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java
new file mode 100644
index 0000000..6d185fe
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java
@@ -0,0 +1,385 @@
+package com.ruoyi.generator.domain;
+
+import java.util.List;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import org.apache.commons.lang3.ArrayUtils;
+import com.ruoyi.common.constant.GenConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 业务表 gen_table
+ *
+ * @author ruoyi
+ */
+public class GenTable extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 编号 */
+ private Long tableId;
+
+ /** 表名称 */
+ @NotBlank(message = "表名称不能为空")
+ private String tableName;
+
+ /** 表描述 */
+ @NotBlank(message = "表描述不能为空")
+ private String tableComment;
+
+ /** 关联父表的表名 */
+ private String subTableName;
+
+ /** 本表关联父表的外键名 */
+ private String subTableFkName;
+
+ /** 实体类名称(首字母大写) */
+ @NotBlank(message = "实体类名称不能为空")
+ private String className;
+
+ /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */
+ private String tplCategory;
+
+ /** 前端类型(element-ui模版 element-plus模版) */
+ private String tplWebType;
+
+ /** 生成包路径 */
+ @NotBlank(message = "生成包路径不能为空")
+ private String packageName;
+
+ /** 生成模块名 */
+ @NotBlank(message = "生成模块名不能为空")
+ private String moduleName;
+
+ /** 生成业务名 */
+ @NotBlank(message = "生成业务名不能为空")
+ private String businessName;
+
+ /** 生成功能名 */
+ @NotBlank(message = "生成功能名不能为空")
+ private String functionName;
+
+ /** 生成作者 */
+ @NotBlank(message = "作者不能为空")
+ private String functionAuthor;
+
+ /** 生成代码方式(0zip压缩包 1自定义路径) */
+ private String genType;
+
+ /** 生成路径(不填默认项目路径) */
+ private String genPath;
+
+ /** 主键信息 */
+ private GenTableColumn pkColumn;
+
+ /** 子表信息 */
+ private GenTable subTable;
+
+ /** 表列信息 */
+ @Valid
+ private List<GenTableColumn> columns;
+
+ /** 其它生成选项 */
+ private String options;
+
+ /** 树编码字段 */
+ private String treeCode;
+
+ /** 树父编码字段 */
+ private String treeParentCode;
+
+ /** 树名称字段 */
+ private String treeName;
+
+ /** 上级菜单ID字段 */
+ private String parentMenuId;
+
+ /** 上级菜单名称字段 */
+ private String parentMenuName;
+
+ public Long getTableId()
+ {
+ return tableId;
+ }
+
+ public void setTableId(Long tableId)
+ {
+ this.tableId = tableId;
+ }
+
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+ public String getTableComment()
+ {
+ return tableComment;
+ }
+
+ public void setTableComment(String tableComment)
+ {
+ this.tableComment = tableComment;
+ }
+
+ public String getSubTableName()
+ {
+ return subTableName;
+ }
+
+ public void setSubTableName(String subTableName)
+ {
+ this.subTableName = subTableName;
+ }
+
+ public String getSubTableFkName()
+ {
+ return subTableFkName;
+ }
+
+ public void setSubTableFkName(String subTableFkName)
+ {
+ this.subTableFkName = subTableFkName;
+ }
+
+ public String getClassName()
+ {
+ return className;
+ }
+
+ public void setClassName(String className)
+ {
+ this.className = className;
+ }
+
+ public String getTplCategory()
+ {
+ return tplCategory;
+ }
+
+ public void setTplCategory(String tplCategory)
+ {
+ this.tplCategory = tplCategory;
+ }
+
+ public String getTplWebType()
+ {
+ return tplWebType;
+ }
+
+ public void setTplWebType(String tplWebType)
+ {
+ this.tplWebType = tplWebType;
+ }
+
+ public String getPackageName()
+ {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName)
+ {
+ this.packageName = packageName;
+ }
+
+ public String getModuleName()
+ {
+ return moduleName;
+ }
+
+ public void setModuleName(String moduleName)
+ {
+ this.moduleName = moduleName;
+ }
+
+ public String getBusinessName()
+ {
+ return businessName;
+ }
+
+ public void setBusinessName(String businessName)
+ {
+ this.businessName = businessName;
+ }
+
+ public String getFunctionName()
+ {
+ return functionName;
+ }
+
+ public void setFunctionName(String functionName)
+ {
+ this.functionName = functionName;
+ }
+
+ public String getFunctionAuthor()
+ {
+ return functionAuthor;
+ }
+
+ public void setFunctionAuthor(String functionAuthor)
+ {
+ this.functionAuthor = functionAuthor;
+ }
+
+ public String getGenType()
+ {
+ return genType;
+ }
+
+ public void setGenType(String genType)
+ {
+ this.genType = genType;
+ }
+
+ public String getGenPath()
+ {
+ return genPath;
+ }
+
+ public void setGenPath(String genPath)
+ {
+ this.genPath = genPath;
+ }
+
+ public GenTableColumn getPkColumn()
+ {
+ return pkColumn;
+ }
+
+ public void setPkColumn(GenTableColumn pkColumn)
+ {
+ this.pkColumn = pkColumn;
+ }
+
+ public GenTable getSubTable()
+ {
+ return subTable;
+ }
+
+ public void setSubTable(GenTable subTable)
+ {
+ this.subTable = subTable;
+ }
+
+ public List<GenTableColumn> getColumns()
+ {
+ return columns;
+ }
+
+ public void setColumns(List<GenTableColumn> columns)
+ {
+ this.columns = columns;
+ }
+
+ public String getOptions()
+ {
+ return options;
+ }
+
+ public void setOptions(String options)
+ {
+ this.options = options;
+ }
+
+ public String getTreeCode()
+ {
+ return treeCode;
+ }
+
+ public void setTreeCode(String treeCode)
+ {
+ this.treeCode = treeCode;
+ }
+
+ public String getTreeParentCode()
+ {
+ return treeParentCode;
+ }
+
+ public void setTreeParentCode(String treeParentCode)
+ {
+ this.treeParentCode = treeParentCode;
+ }
+
+ public String getTreeName()
+ {
+ return treeName;
+ }
+
+ public void setTreeName(String treeName)
+ {
+ this.treeName = treeName;
+ }
+
+ public String getParentMenuId()
+ {
+ return parentMenuId;
+ }
+
+ public void setParentMenuId(String parentMenuId)
+ {
+ this.parentMenuId = parentMenuId;
+ }
+
+ public String getParentMenuName()
+ {
+ return parentMenuName;
+ }
+
+ public void setParentMenuName(String parentMenuName)
+ {
+ this.parentMenuName = parentMenuName;
+ }
+
+ public boolean isSub()
+ {
+ return isSub(this.tplCategory);
+ }
+
+ public static boolean isSub(String tplCategory)
+ {
+ return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory);
+ }
+
+ public boolean isTree()
+ {
+ return isTree(this.tplCategory);
+ }
+
+ public static boolean isTree(String tplCategory)
+ {
+ return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory);
+ }
+
+ public boolean isCrud()
+ {
+ return isCrud(this.tplCategory);
+ }
+
+ public static boolean isCrud(String tplCategory)
+ {
+ return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory);
+ }
+
+ public boolean isSuperColumn(String javaField)
+ {
+ return isSuperColumn(this.tplCategory, javaField);
+ }
+
+ public static boolean isSuperColumn(String tplCategory, String javaField)
+ {
+ if (isTree(tplCategory))
+ {
+ return StringUtils.equalsAnyIgnoreCase(javaField,
+ ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY));
+ }
+ return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java
new file mode 100644
index 0000000..3dc6d62
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java
@@ -0,0 +1,373 @@
+package com.ruoyi.generator.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 代码生成业务字段表 gen_table_column
+ *
+ * @author ruoyi
+ */
+public class GenTableColumn extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 编号 */
+ private Long columnId;
+
+ /** 归属表编号 */
+ private Long tableId;
+
+ /** 列名称 */
+ private String columnName;
+
+ /** 列描述 */
+ private String columnComment;
+
+ /** 列类型 */
+ private String columnType;
+
+ /** JAVA类型 */
+ private String javaType;
+
+ /** JAVA字段名 */
+ @NotBlank(message = "Java属性不能为空")
+ private String javaField;
+
+ /** 是否主键(1是) */
+ private String isPk;
+
+ /** 是否自增(1是) */
+ private String isIncrement;
+
+ /** 是否必填(1是) */
+ private String isRequired;
+
+ /** 是否为插入字段(1是) */
+ private String isInsert;
+
+ /** 是否编辑字段(1是) */
+ private String isEdit;
+
+ /** 是否列表字段(1是) */
+ private String isList;
+
+ /** 是否查询字段(1是) */
+ private String isQuery;
+
+ /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */
+ private String queryType;
+
+ /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */
+ private String htmlType;
+
+ /** 字典类型 */
+ private String dictType;
+
+ /** 排序 */
+ private Integer sort;
+
+ public void setColumnId(Long columnId)
+ {
+ this.columnId = columnId;
+ }
+
+ public Long getColumnId()
+ {
+ return columnId;
+ }
+
+ public void setTableId(Long tableId)
+ {
+ this.tableId = tableId;
+ }
+
+ public Long getTableId()
+ {
+ return tableId;
+ }
+
+ public void setColumnName(String columnName)
+ {
+ this.columnName = columnName;
+ }
+
+ public String getColumnName()
+ {
+ return columnName;
+ }
+
+ public void setColumnComment(String columnComment)
+ {
+ this.columnComment = columnComment;
+ }
+
+ public String getColumnComment()
+ {
+ return columnComment;
+ }
+
+ public void setColumnType(String columnType)
+ {
+ this.columnType = columnType;
+ }
+
+ public String getColumnType()
+ {
+ return columnType;
+ }
+
+ public void setJavaType(String javaType)
+ {
+ this.javaType = javaType;
+ }
+
+ public String getJavaType()
+ {
+ return javaType;
+ }
+
+ public void setJavaField(String javaField)
+ {
+ this.javaField = javaField;
+ }
+
+ public String getJavaField()
+ {
+ return javaField;
+ }
+
+ public String getCapJavaField()
+ {
+ return StringUtils.capitalize(javaField);
+ }
+
+ public void setIsPk(String isPk)
+ {
+ this.isPk = isPk;
+ }
+
+ public String getIsPk()
+ {
+ return isPk;
+ }
+
+ public boolean isPk()
+ {
+ return isPk(this.isPk);
+ }
+
+ public boolean isPk(String isPk)
+ {
+ return isPk != null && StringUtils.equals("1", isPk);
+ }
+
+ public String getIsIncrement()
+ {
+ return isIncrement;
+ }
+
+ public void setIsIncrement(String isIncrement)
+ {
+ this.isIncrement = isIncrement;
+ }
+
+ public boolean isIncrement()
+ {
+ return isIncrement(this.isIncrement);
+ }
+
+ public boolean isIncrement(String isIncrement)
+ {
+ return isIncrement != null && StringUtils.equals("1", isIncrement);
+ }
+
+ public void setIsRequired(String isRequired)
+ {
+ this.isRequired = isRequired;
+ }
+
+ public String getIsRequired()
+ {
+ return isRequired;
+ }
+
+ public boolean isRequired()
+ {
+ return isRequired(this.isRequired);
+ }
+
+ public boolean isRequired(String isRequired)
+ {
+ return isRequired != null && StringUtils.equals("1", isRequired);
+ }
+
+ public void setIsInsert(String isInsert)
+ {
+ this.isInsert = isInsert;
+ }
+
+ public String getIsInsert()
+ {
+ return isInsert;
+ }
+
+ public boolean isInsert()
+ {
+ return isInsert(this.isInsert);
+ }
+
+ public boolean isInsert(String isInsert)
+ {
+ return isInsert != null && StringUtils.equals("1", isInsert);
+ }
+
+ public void setIsEdit(String isEdit)
+ {
+ this.isEdit = isEdit;
+ }
+
+ public String getIsEdit()
+ {
+ return isEdit;
+ }
+
+ public boolean isEdit()
+ {
+ return isInsert(this.isEdit);
+ }
+
+ public boolean isEdit(String isEdit)
+ {
+ return isEdit != null && StringUtils.equals("1", isEdit);
+ }
+
+ public void setIsList(String isList)
+ {
+ this.isList = isList;
+ }
+
+ public String getIsList()
+ {
+ return isList;
+ }
+
+ public boolean isList()
+ {
+ return isList(this.isList);
+ }
+
+ public boolean isList(String isList)
+ {
+ return isList != null && StringUtils.equals("1", isList);
+ }
+
+ public void setIsQuery(String isQuery)
+ {
+ this.isQuery = isQuery;
+ }
+
+ public String getIsQuery()
+ {
+ return isQuery;
+ }
+
+ public boolean isQuery()
+ {
+ return isQuery(this.isQuery);
+ }
+
+ public boolean isQuery(String isQuery)
+ {
+ return isQuery != null && StringUtils.equals("1", isQuery);
+ }
+
+ public void setQueryType(String queryType)
+ {
+ this.queryType = queryType;
+ }
+
+ public String getQueryType()
+ {
+ return queryType;
+ }
+
+ public String getHtmlType()
+ {
+ return htmlType;
+ }
+
+ public void setHtmlType(String htmlType)
+ {
+ this.htmlType = htmlType;
+ }
+
+ public void setDictType(String dictType)
+ {
+ this.dictType = dictType;
+ }
+
+ public String getDictType()
+ {
+ return dictType;
+ }
+
+ public void setSort(Integer sort)
+ {
+ this.sort = sort;
+ }
+
+ public Integer getSort()
+ {
+ return sort;
+ }
+
+ public boolean isSuperColumn()
+ {
+ return isSuperColumn(this.javaField);
+ }
+
+ public static boolean isSuperColumn(String javaField)
+ {
+ return StringUtils.equalsAnyIgnoreCase(javaField,
+ // BaseEntity
+ "createBy", "createTime", "updateBy", "updateTime", "remark",
+ // TreeEntity
+ "parentName", "parentId", "orderNum", "ancestors");
+ }
+
+ public boolean isUsableColumn()
+ {
+ return isUsableColumn(javaField);
+ }
+
+ public static boolean isUsableColumn(String javaField)
+ {
+ // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单
+ return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark");
+ }
+
+ public String readConverterExp()
+ {
+ String remarks = StringUtils.substringBetween(this.columnComment, "(", ")");
+ StringBuffer sb = new StringBuffer();
+ if (StringUtils.isNotEmpty(remarks))
+ {
+ for (String value : remarks.split(" "))
+ {
+ if (StringUtils.isNotEmpty(value))
+ {
+ Object startStr = value.subSequence(0, 1);
+ String endStr = value.substring(1);
+ sb.append("").append(startStr).append("=").append(endStr).append(",");
+ }
+ }
+ return sb.deleteCharAt(sb.length() - 1).toString();
+ }
+ else
+ {
+ return this.columnComment;
+ }
+ }
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java
new file mode 100644
index 0000000..951e166
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java
@@ -0,0 +1,60 @@
+package com.ruoyi.generator.mapper;
+
+import java.util.List;
+import com.ruoyi.generator.domain.GenTableColumn;
+
+/**
+ * 业务字段 数据层
+ *
+ * @author ruoyi
+ */
+public interface GenTableColumnMapper
+{
+ /**
+ * 根据表名称查询列信息
+ *
+ * @param tableName 表名称
+ * @return 列信息
+ */
+ public List<GenTableColumn> selectDbTableColumnsByName(String tableName);
+
+ /**
+ * 查询业务字段列表
+ *
+ * @param tableId 业务字段编号
+ * @return 业务字段集合
+ */
+ public List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId);
+
+ /**
+ * 新增业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ public int insertGenTableColumn(GenTableColumn genTableColumn);
+
+ /**
+ * 修改业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ public int updateGenTableColumn(GenTableColumn genTableColumn);
+
+ /**
+ * 删除业务字段
+ *
+ * @param genTableColumns 列数据
+ * @return 结果
+ */
+ public int deleteGenTableColumns(List<GenTableColumn> genTableColumns);
+
+ /**
+ * 批量删除业务字段
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteGenTableColumnByIds(Long[] ids);
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java
new file mode 100644
index 0000000..937656d
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java
@@ -0,0 +1,91 @@
+package com.ruoyi.generator.mapper;
+
+import java.util.List;
+import com.ruoyi.generator.domain.GenTable;
+
+/**
+ * 业务 数据层
+ *
+ * @author ruoyi
+ */
+public interface GenTableMapper
+{
+ /**
+ * 查询业务列表
+ *
+ * @param genTable 业务信息
+ * @return 业务集合
+ */
+ public List<GenTable> selectGenTableList(GenTable genTable);
+
+ /**
+ * 查询据库列表
+ *
+ * @param genTable 业务信息
+ * @return 数据库表集合
+ */
+ public List<GenTable> selectDbTableList(GenTable genTable);
+
+ /**
+ * 查询据库列表
+ *
+ * @param tableNames 表名称组
+ * @return 数据库表集合
+ */
+ public List<GenTable> selectDbTableListByNames(String[] tableNames);
+
+ /**
+ * 查询所有表信息
+ *
+ * @return 表信息集合
+ */
+ public List<GenTable> selectGenTableAll();
+
+ /**
+ * 查询表ID业务信息
+ *
+ * @param id 业务ID
+ * @return 业务信息
+ */
+ public GenTable selectGenTableById(Long id);
+
+ /**
+ * 查询表名称业务信息
+ *
+ * @param tableName 表名称
+ * @return 业务信息
+ */
+ public GenTable selectGenTableByName(String tableName);
+
+ /**
+ * 新增业务
+ *
+ * @param genTable 业务信息
+ * @return 结果
+ */
+ public int insertGenTable(GenTable genTable);
+
+ /**
+ * 修改业务
+ *
+ * @param genTable 业务信息
+ * @return 结果
+ */
+ public int updateGenTable(GenTable genTable);
+
+ /**
+ * 批量删除业务
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteGenTableByIds(Long[] ids);
+
+ /**
+ * 创建表
+ *
+ * @param sql 表结构
+ * @return 结果
+ */
+ public int createTable(String sql);
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java
new file mode 100644
index 0000000..0679689
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java
@@ -0,0 +1,68 @@
+package com.ruoyi.generator.service;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.generator.domain.GenTableColumn;
+import com.ruoyi.generator.mapper.GenTableColumnMapper;
+
+/**
+ * 业务字段 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class GenTableColumnServiceImpl implements IGenTableColumnService
+{
+ @Autowired
+ private GenTableColumnMapper genTableColumnMapper;
+
+ /**
+ * 查询业务字段列表
+ *
+ * @param tableId 业务字段编号
+ * @return 业务字段集合
+ */
+ @Override
+ public List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId)
+ {
+ return genTableColumnMapper.selectGenTableColumnListByTableId(tableId);
+ }
+
+ /**
+ * 新增业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ @Override
+ public int insertGenTableColumn(GenTableColumn genTableColumn)
+ {
+ return genTableColumnMapper.insertGenTableColumn(genTableColumn);
+ }
+
+ /**
+ * 修改业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ @Override
+ public int updateGenTableColumn(GenTableColumn genTableColumn)
+ {
+ return genTableColumnMapper.updateGenTableColumn(genTableColumn);
+ }
+
+ /**
+ * 删除业务字段对象
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ @Override
+ public int deleteGenTableColumnByIds(String ids)
+ {
+ return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids));
+ }
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
new file mode 100644
index 0000000..3fb6383
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
@@ -0,0 +1,531 @@
+package com.ruoyi.generator.service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.GenConstants;
+import com.ruoyi.common.core.text.CharsetKit;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.generator.domain.GenTable;
+import com.ruoyi.generator.domain.GenTableColumn;
+import com.ruoyi.generator.mapper.GenTableColumnMapper;
+import com.ruoyi.generator.mapper.GenTableMapper;
+import com.ruoyi.generator.util.GenUtils;
+import com.ruoyi.generator.util.VelocityInitializer;
+import com.ruoyi.generator.util.VelocityUtils;
+
+/**
+ * 业务 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class GenTableServiceImpl implements IGenTableService
+{
+ private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class);
+
+ @Autowired
+ private GenTableMapper genTableMapper;
+
+ @Autowired
+ private GenTableColumnMapper genTableColumnMapper;
+
+ /**
+ * 查询业务信息
+ *
+ * @param id 业务ID
+ * @return 业务信息
+ */
+ @Override
+ public GenTable selectGenTableById(Long id)
+ {
+ GenTable genTable = genTableMapper.selectGenTableById(id);
+ setTableFromOptions(genTable);
+ return genTable;
+ }
+
+ /**
+ * 查询业务列表
+ *
+ * @param genTable 业务信息
+ * @return 业务集合
+ */
+ @Override
+ public List<GenTable> selectGenTableList(GenTable genTable)
+ {
+ return genTableMapper.selectGenTableList(genTable);
+ }
+
+ /**
+ * 查询据库列表
+ *
+ * @param genTable 业务信息
+ * @return 数据库表集合
+ */
+ @Override
+ public List<GenTable> selectDbTableList(GenTable genTable)
+ {
+ return genTableMapper.selectDbTableList(genTable);
+ }
+
+ /**
+ * 查询据库列表
+ *
+ * @param tableNames 表名称组
+ * @return 数据库表集合
+ */
+ @Override
+ public List<GenTable> selectDbTableListByNames(String[] tableNames)
+ {
+ return genTableMapper.selectDbTableListByNames(tableNames);
+ }
+
+ /**
+ * 查询所有表信息
+ *
+ * @return 表信息集合
+ */
+ @Override
+ public List<GenTable> selectGenTableAll()
+ {
+ return genTableMapper.selectGenTableAll();
+ }
+
+ /**
+ * 修改业务
+ *
+ * @param genTable 业务信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public void updateGenTable(GenTable genTable)
+ {
+ String options = JSON.toJSONString(genTable.getParams());
+ genTable.setOptions(options);
+ int row = genTableMapper.updateGenTable(genTable);
+ if (row > 0)
+ {
+ for (GenTableColumn cenTableColumn : genTable.getColumns())
+ {
+ genTableColumnMapper.updateGenTableColumn(cenTableColumn);
+ }
+ }
+ }
+
+ /**
+ * 删除业务对象
+ *
+ * @param tableIds 需要删除的数据ID
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public void deleteGenTableByIds(Long[] tableIds)
+ {
+ genTableMapper.deleteGenTableByIds(tableIds);
+ genTableColumnMapper.deleteGenTableColumnByIds(tableIds);
+ }
+
+ /**
+ * 创建表
+ *
+ * @param sql 创建表语句
+ * @return 结果
+ */
+ @Override
+ public boolean createTable(String sql)
+ {
+ return genTableMapper.createTable(sql) == 0;
+ }
+
+ /**
+ * 导入表结构
+ *
+ * @param tableList 导入表列表
+ */
+ @Override
+ @Transactional
+ public void importGenTable(List<GenTable> tableList, String operName)
+ {
+ try
+ {
+ for (GenTable table : tableList)
+ {
+ String tableName = table.getTableName();
+ GenUtils.initTable(table, operName);
+ int row = genTableMapper.insertGenTable(table);
+ if (row > 0)
+ {
+ // 保存列信息
+ List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
+ for (GenTableColumn column : genTableColumns)
+ {
+ GenUtils.initColumnField(column, table);
+ genTableColumnMapper.insertGenTableColumn(column);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("导入失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 预览代码
+ *
+ * @param tableId 表编号
+ * @return 预览数据列表
+ */
+ @Override
+ public Map<String, String> previewCode(Long tableId)
+ {
+ Map<String, String> dataMap = new LinkedHashMap<>();
+ // 查询表信息
+ GenTable table = genTableMapper.selectGenTableById(tableId);
+ // 设置主子表信息
+ setSubTable(table);
+ // 设置主键列信息
+ setPkColumn(table);
+ VelocityInitializer.initVelocity();
+
+ VelocityContext context = VelocityUtils.prepareContext(table);
+
+ // 获取模板列表
+ List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
+ for (String template : templates)
+ {
+ // 渲染模板
+ StringWriter sw = new StringWriter();
+ Template tpl = Velocity.getTemplate(template, Constants.UTF8);
+ tpl.merge(context, sw);
+ dataMap.put(template, sw.toString());
+ }
+ return dataMap;
+ }
+
+ /**
+ * 生成代码(下载方式)
+ *
+ * @param tableName 表名称
+ * @return 数据
+ */
+ @Override
+ public byte[] downloadCode(String tableName)
+ {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ZipOutputStream zip = new ZipOutputStream(outputStream);
+ generatorCode(tableName, zip);
+ IOUtils.closeQuietly(zip);
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * 生成代码(自定义路径)
+ *
+ * @param tableName 表名称
+ */
+ @Override
+ public void generatorCode(String tableName)
+ {
+ // 查询表信息
+ GenTable table = genTableMapper.selectGenTableByName(tableName);
+ // 设置主子表信息
+ setSubTable(table);
+ // 设置主键列信息
+ setPkColumn(table);
+
+ VelocityInitializer.initVelocity();
+
+ VelocityContext context = VelocityUtils.prepareContext(table);
+
+ // 获取模板列表
+ List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
+ for (String template : templates)
+ {
+ if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm"))
+ {
+ // 渲染模板
+ StringWriter sw = new StringWriter();
+ Template tpl = Velocity.getTemplate(template, Constants.UTF8);
+ tpl.merge(context, sw);
+ try
+ {
+ String path = getGenPath(table, template);
+ FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8);
+ }
+ catch (IOException e)
+ {
+ throw new ServiceException("渲染模板失败,表名:" + table.getTableName());
+ }
+ }
+ }
+ }
+
+ /**
+ * 同步数据库
+ *
+ * @param tableName 表名称
+ */
+ @Override
+ @Transactional
+ public void synchDb(String tableName)
+ {
+ GenTable table = genTableMapper.selectGenTableByName(tableName);
+ List<GenTableColumn> tableColumns = table.getColumns();
+ Map<String, GenTableColumn> tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity()));
+
+ List<GenTableColumn> dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
+ if (StringUtils.isEmpty(dbTableColumns))
+ {
+ throw new ServiceException("同步数据失败,原表结构不存在");
+ }
+ List<String> dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList());
+
+ dbTableColumns.forEach(column -> {
+ GenUtils.initColumnField(column, table);
+ if (tableColumnMap.containsKey(column.getColumnName()))
+ {
+ GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName());
+ column.setColumnId(prevColumn.getColumnId());
+ if (column.isList())
+ {
+ // 如果是列表,继续保留查询方式/字典类型选项
+ column.setDictType(prevColumn.getDictType());
+ column.setQueryType(prevColumn.getQueryType());
+ }
+ if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk()
+ && (column.isInsert() || column.isEdit())
+ && ((column.isUsableColumn()) || (!column.isSuperColumn())))
+ {
+ // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项
+ column.setIsRequired(prevColumn.getIsRequired());
+ column.setHtmlType(prevColumn.getHtmlType());
+ }
+ genTableColumnMapper.updateGenTableColumn(column);
+ }
+ else
+ {
+ genTableColumnMapper.insertGenTableColumn(column);
+ }
+ });
+
+ List<GenTableColumn> delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList());
+ if (StringUtils.isNotEmpty(delColumns))
+ {
+ genTableColumnMapper.deleteGenTableColumns(delColumns);
+ }
+ }
+
+ /**
+ * 批量生成代码(下载方式)
+ *
+ * @param tableNames 表数组
+ * @return 数据
+ */
+ @Override
+ public byte[] downloadCode(String[] tableNames)
+ {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ZipOutputStream zip = new ZipOutputStream(outputStream);
+ for (String tableName : tableNames)
+ {
+ generatorCode(tableName, zip);
+ }
+ IOUtils.closeQuietly(zip);
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * 查询表信息并生成代码
+ */
+ private void generatorCode(String tableName, ZipOutputStream zip)
+ {
+ // 查询表信息
+ GenTable table = genTableMapper.selectGenTableByName(tableName);
+ // 设置主子表信息
+ setSubTable(table);
+ // 设置主键列信息
+ setPkColumn(table);
+
+ VelocityInitializer.initVelocity();
+
+ VelocityContext context = VelocityUtils.prepareContext(table);
+
+ // 获取模板列表
+ List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
+ for (String template : templates)
+ {
+ // 渲染模板
+ StringWriter sw = new StringWriter();
+ Template tpl = Velocity.getTemplate(template, Constants.UTF8);
+ tpl.merge(context, sw);
+ try
+ {
+ // 添加到zip
+ zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table)));
+ IOUtils.write(sw.toString(), zip, Constants.UTF8);
+ IOUtils.closeQuietly(sw);
+ zip.flush();
+ zip.closeEntry();
+ }
+ catch (IOException e)
+ {
+ log.error("渲染模板失败,表名:" + table.getTableName(), e);
+ }
+ }
+ }
+
+ /**
+ * 修改保存参数校验
+ *
+ * @param genTable 业务信息
+ */
+ @Override
+ public void validateEdit(GenTable genTable)
+ {
+ if (GenConstants.TPL_TREE.equals(genTable.getTplCategory()))
+ {
+ String options = JSON.toJSONString(genTable.getParams());
+ JSONObject paramsObj = JSON.parseObject(options);
+ if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE)))
+ {
+ throw new ServiceException("树编码字段不能为空");
+ }
+ else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE)))
+ {
+ throw new ServiceException("树父编码字段不能为空");
+ }
+ else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME)))
+ {
+ throw new ServiceException("树名称字段不能为空");
+ }
+ else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory()))
+ {
+ if (StringUtils.isEmpty(genTable.getSubTableName()))
+ {
+ throw new ServiceException("关联子表的表名不能为空");
+ }
+ else if (StringUtils.isEmpty(genTable.getSubTableFkName()))
+ {
+ throw new ServiceException("子表关联的外键名不能为空");
+ }
+ }
+ }
+ }
+
+ /**
+ * 设置主键列信息
+ *
+ * @param table 业务表信息
+ */
+ public void setPkColumn(GenTable table)
+ {
+ for (GenTableColumn column : table.getColumns())
+ {
+ if (column.isPk())
+ {
+ table.setPkColumn(column);
+ break;
+ }
+ }
+ if (StringUtils.isNull(table.getPkColumn()))
+ {
+ table.setPkColumn(table.getColumns().get(0));
+ }
+ if (GenConstants.TPL_SUB.equals(table.getTplCategory()))
+ {
+ for (GenTableColumn column : table.getSubTable().getColumns())
+ {
+ if (column.isPk())
+ {
+ table.getSubTable().setPkColumn(column);
+ break;
+ }
+ }
+ if (StringUtils.isNull(table.getSubTable().getPkColumn()))
+ {
+ table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0));
+ }
+ }
+ }
+
+ /**
+ * 设置主子表信息
+ *
+ * @param table 业务表信息
+ */
+ public void setSubTable(GenTable table)
+ {
+ String subTableName = table.getSubTableName();
+ if (StringUtils.isNotEmpty(subTableName))
+ {
+ table.setSubTable(genTableMapper.selectGenTableByName(subTableName));
+ }
+ }
+
+ /**
+ * 设置代码生成其他选项值
+ *
+ * @param genTable 设置后的生成对象
+ */
+ public void setTableFromOptions(GenTable genTable)
+ {
+ JSONObject paramsObj = JSON.parseObject(genTable.getOptions());
+ if (StringUtils.isNotNull(paramsObj))
+ {
+ String treeCode = paramsObj.getString(GenConstants.TREE_CODE);
+ String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE);
+ String treeName = paramsObj.getString(GenConstants.TREE_NAME);
+ String parentMenuId = paramsObj.getString(GenConstants.PARENT_MENU_ID);
+ String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME);
+
+ genTable.setTreeCode(treeCode);
+ genTable.setTreeParentCode(treeParentCode);
+ genTable.setTreeName(treeName);
+ genTable.setParentMenuId(parentMenuId);
+ genTable.setParentMenuName(parentMenuName);
+ }
+ }
+
+ /**
+ * 获取代码生成地址
+ *
+ * @param table 业务表信息
+ * @param template 模板文件路径
+ * @return 生成地址
+ */
+ public static String getGenPath(GenTable table, String template)
+ {
+ String genPath = table.getGenPath();
+ if (StringUtils.equals(genPath, "/"))
+ {
+ return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table);
+ }
+ return genPath + File.separator + VelocityUtils.getFileName(template, table);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java
new file mode 100644
index 0000000..3037f70
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java
@@ -0,0 +1,44 @@
+package com.ruoyi.generator.service;
+
+import java.util.List;
+import com.ruoyi.generator.domain.GenTableColumn;
+
+/**
+ * 业务字段 服务层
+ *
+ * @author ruoyi
+ */
+public interface IGenTableColumnService
+{
+ /**
+ * 查询业务字段列表
+ *
+ * @param tableId 业务字段编号
+ * @return 业务字段集合
+ */
+ public List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId);
+
+ /**
+ * 新增业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ public int insertGenTableColumn(GenTableColumn genTableColumn);
+
+ /**
+ * 修改业务字段
+ *
+ * @param genTableColumn 业务字段信息
+ * @return 结果
+ */
+ public int updateGenTableColumn(GenTableColumn genTableColumn);
+
+ /**
+ * 删除业务字段信息
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteGenTableColumnByIds(String ids);
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java
new file mode 100644
index 0000000..695426e
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java
@@ -0,0 +1,130 @@
+package com.ruoyi.generator.service;
+
+import java.util.List;
+import java.util.Map;
+import com.ruoyi.generator.domain.GenTable;
+
+/**
+ * 业务 服务层
+ *
+ * @author ruoyi
+ */
+public interface IGenTableService
+{
+ /**
+ * 查询业务列表
+ *
+ * @param genTable 业务信息
+ * @return 业务集合
+ */
+ public List<GenTable> selectGenTableList(GenTable genTable);
+
+ /**
+ * 查询据库列表
+ *
+ * @param genTable 业务信息
+ * @return 数据库表集合
+ */
+ public List<GenTable> selectDbTableList(GenTable genTable);
+
+ /**
+ * 查询据库列表
+ *
+ * @param tableNames 表名称组
+ * @return 数据库表集合
+ */
+ public List<GenTable> selectDbTableListByNames(String[] tableNames);
+
+ /**
+ * 查询所有表信息
+ *
+ * @return 表信息集合
+ */
+ public List<GenTable> selectGenTableAll();
+
+ /**
+ * 查询业务信息
+ *
+ * @param id 业务ID
+ * @return 业务信息
+ */
+ public GenTable selectGenTableById(Long id);
+
+ /**
+ * 修改业务
+ *
+ * @param genTable 业务信息
+ * @return 结果
+ */
+ public void updateGenTable(GenTable genTable);
+
+ /**
+ * 删除业务信息
+ *
+ * @param tableIds 需要删除的表数据ID
+ * @return 结果
+ */
+ public void deleteGenTableByIds(Long[] tableIds);
+
+ /**
+ * 创建表
+ *
+ * @param sql 创建表语句
+ * @return 结果
+ */
+ public boolean createTable(String sql);
+
+ /**
+ * 导入表结构
+ *
+ * @param tableList 导入表列表
+ * @param operName 操作人员
+ */
+ public void importGenTable(List<GenTable> tableList, String operName);
+
+ /**
+ * 预览代码
+ *
+ * @param tableId 表编号
+ * @return 预览数据列表
+ */
+ public Map<String, String> previewCode(Long tableId);
+
+ /**
+ * 生成代码(下载方式)
+ *
+ * @param tableName 表名称
+ * @return 数据
+ */
+ public byte[] downloadCode(String tableName);
+
+ /**
+ * 生成代码(自定义路径)
+ *
+ * @param tableName 表名称
+ * @return 数据
+ */
+ public void generatorCode(String tableName);
+
+ /**
+ * 同步数据库
+ *
+ * @param tableName 表名称
+ */
+ public void synchDb(String tableName);
+
+ /**
+ * 批量生成代码(下载方式)
+ *
+ * @param tableNames 表数组
+ * @return 数据
+ */
+ public byte[] downloadCode(String[] tableNames);
+
+ /**
+ * 修改保存参数校验
+ *
+ * @param genTable 业务信息
+ */
+ public void validateEdit(GenTable genTable);
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java
new file mode 100644
index 0000000..e7ebc20
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java
@@ -0,0 +1,257 @@
+package com.ruoyi.generator.util;
+
+import java.util.Arrays;
+import org.apache.commons.lang3.RegExUtils;
+import com.ruoyi.common.constant.GenConstants;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.generator.config.GenConfig;
+import com.ruoyi.generator.domain.GenTable;
+import com.ruoyi.generator.domain.GenTableColumn;
+
+/**
+ * 代码生成器 工具类
+ *
+ * @author ruoyi
+ */
+public class GenUtils
+{
+ /**
+ * 初始化表信息
+ */
+ public static void initTable(GenTable genTable, String operName)
+ {
+ genTable.setClassName(convertClassName(genTable.getTableName()));
+ genTable.setPackageName(GenConfig.getPackageName());
+ genTable.setModuleName(getModuleName(GenConfig.getPackageName()));
+ genTable.setBusinessName(getBusinessName(genTable.getTableName()));
+ genTable.setFunctionName(replaceText(genTable.getTableComment()));
+ genTable.setFunctionAuthor(GenConfig.getAuthor());
+ genTable.setCreateBy(operName);
+ }
+
+ /**
+ * 初始化列属性字段
+ */
+ public static void initColumnField(GenTableColumn column, GenTable table)
+ {
+ String dataType = getDbType(column.getColumnType());
+ String columnName = column.getColumnName();
+ column.setTableId(table.getTableId());
+ column.setCreateBy(table.getCreateBy());
+ // 设置java字段名
+ column.setJavaField(StringUtils.toCamelCase(columnName));
+ // 设置默认类型
+ column.setJavaType(GenConstants.TYPE_STRING);
+ column.setQueryType(GenConstants.QUERY_EQ);
+
+ if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType))
+ {
+ // 字符串长度超过500设置为文本域
+ Integer columnLength = getColumnLength(column.getColumnType());
+ String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT;
+ column.setHtmlType(htmlType);
+ }
+ else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType))
+ {
+ column.setJavaType(GenConstants.TYPE_DATE);
+ column.setHtmlType(GenConstants.HTML_DATETIME);
+ }
+ else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType))
+ {
+ column.setHtmlType(GenConstants.HTML_INPUT);
+
+ // 如果是浮点型 统一用BigDecimal
+ String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ",");
+ if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0)
+ {
+ column.setJavaType(GenConstants.TYPE_BIGDECIMAL);
+ }
+ // 如果是整形
+ else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10)
+ {
+ column.setJavaType(GenConstants.TYPE_INTEGER);
+ }
+ // 长整形
+ else
+ {
+ column.setJavaType(GenConstants.TYPE_LONG);
+ }
+ }
+
+ // 插入字段(默认所有字段都需要插入)
+ column.setIsInsert(GenConstants.REQUIRE);
+
+ // 编辑字段
+ if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk())
+ {
+ column.setIsEdit(GenConstants.REQUIRE);
+ }
+ // 列表字段
+ if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk())
+ {
+ column.setIsList(GenConstants.REQUIRE);
+ }
+ // 查询字段
+ if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk())
+ {
+ column.setIsQuery(GenConstants.REQUIRE);
+ }
+
+ // 查询字段类型
+ if (StringUtils.endsWithIgnoreCase(columnName, "name"))
+ {
+ column.setQueryType(GenConstants.QUERY_LIKE);
+ }
+ // 状态字段设置单选框
+ if (StringUtils.endsWithIgnoreCase(columnName, "status"))
+ {
+ column.setHtmlType(GenConstants.HTML_RADIO);
+ }
+ // 类型&性别字段设置下拉框
+ else if (StringUtils.endsWithIgnoreCase(columnName, "type")
+ || StringUtils.endsWithIgnoreCase(columnName, "sex"))
+ {
+ column.setHtmlType(GenConstants.HTML_SELECT);
+ }
+ // 图片字段设置图片上传控件
+ else if (StringUtils.endsWithIgnoreCase(columnName, "image"))
+ {
+ column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD);
+ }
+ // 文件字段设置文件上传控件
+ else if (StringUtils.endsWithIgnoreCase(columnName, "file"))
+ {
+ column.setHtmlType(GenConstants.HTML_FILE_UPLOAD);
+ }
+ // 内容字段设置富文本控件
+ else if (StringUtils.endsWithIgnoreCase(columnName, "content"))
+ {
+ column.setHtmlType(GenConstants.HTML_EDITOR);
+ }
+ }
+
+ /**
+ * 校验数组是否包含指定值
+ *
+ * @param arr 数组
+ * @param targetValue 值
+ * @return 是否包含
+ */
+ public static boolean arraysContains(String[] arr, String targetValue)
+ {
+ return Arrays.asList(arr).contains(targetValue);
+ }
+
+ /**
+ * 获取模块名
+ *
+ * @param packageName 包名
+ * @return 模块名
+ */
+ public static String getModuleName(String packageName)
+ {
+ int lastIndex = packageName.lastIndexOf(".");
+ int nameLength = packageName.length();
+ return StringUtils.substring(packageName, lastIndex + 1, nameLength);
+ }
+
+ /**
+ * 获取业务名
+ *
+ * @param tableName 表名
+ * @return 业务名
+ */
+ public static String getBusinessName(String tableName)
+ {
+ int lastIndex = tableName.lastIndexOf("_");
+ int nameLength = tableName.length();
+ return StringUtils.substring(tableName, lastIndex + 1, nameLength);
+ }
+
+ /**
+ * 表名转换成Java类名
+ *
+ * @param tableName 表名称
+ * @return 类名
+ */
+ public static String convertClassName(String tableName)
+ {
+ boolean autoRemovePre = GenConfig.getAutoRemovePre();
+ String tablePrefix = GenConfig.getTablePrefix();
+ if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix))
+ {
+ String[] searchList = StringUtils.split(tablePrefix, ",");
+ tableName = replaceFirst(tableName, searchList);
+ }
+ return StringUtils.convertToCamelCase(tableName);
+ }
+
+ /**
+ * 批量替换前缀
+ *
+ * @param replacementm 替换值
+ * @param searchList 替换列表
+ * @return
+ */
+ public static String replaceFirst(String replacementm, String[] searchList)
+ {
+ String text = replacementm;
+ for (String searchString : searchList)
+ {
+ if (replacementm.startsWith(searchString))
+ {
+ text = replacementm.replaceFirst(searchString, "");
+ break;
+ }
+ }
+ return text;
+ }
+
+ /**
+ * 关键字替换
+ *
+ * @param text 需要被替换的名字
+ * @return 替换后的名字
+ */
+ public static String replaceText(String text)
+ {
+ return RegExUtils.replaceAll(text, "(?:表|若依)", "");
+ }
+
+ /**
+ * 获取数据库类型字段
+ *
+ * @param columnType 列类型
+ * @return 截取后的列类型
+ */
+ public static String getDbType(String columnType)
+ {
+ if (StringUtils.indexOf(columnType, "(") > 0)
+ {
+ return StringUtils.substringBefore(columnType, "(");
+ }
+ else
+ {
+ return columnType;
+ }
+ }
+
+ /**
+ * 获取字段长度
+ *
+ * @param columnType 列类型
+ * @return 截取后的列类型
+ */
+ public static Integer getColumnLength(String columnType)
+ {
+ if (StringUtils.indexOf(columnType, "(") > 0)
+ {
+ String length = StringUtils.substringBetween(columnType, "(", ")");
+ return Integer.valueOf(length);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java
new file mode 100644
index 0000000..9f69403
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java
@@ -0,0 +1,34 @@
+package com.ruoyi.generator.util;
+
+import java.util.Properties;
+import org.apache.velocity.app.Velocity;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * VelocityEngine工厂
+ *
+ * @author ruoyi
+ */
+public class VelocityInitializer
+{
+ /**
+ * 初始化vm方法
+ */
+ public static void initVelocity()
+ {
+ Properties p = new Properties();
+ try
+ {
+ // 加载classpath目录下的vm文件
+ p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+ // 定义字符集
+ p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8);
+ // 初始化Velocity引擎,指定配置Properties
+ Velocity.init(p);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java
new file mode 100644
index 0000000..1a14681
--- /dev/null
+++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java
@@ -0,0 +1,408 @@
+package com.ruoyi.generator.util;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.velocity.VelocityContext;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.constant.GenConstants;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.generator.domain.GenTable;
+import com.ruoyi.generator.domain.GenTableColumn;
+
+/**
+ * 模板处理工具类
+ *
+ * @author ruoyi
+ */
+public class VelocityUtils
+{
+ /** 项目空间路径 */
+ private static final String PROJECT_PATH = "main/java";
+
+ /** mybatis空间路径 */
+ private static final String MYBATIS_PATH = "main/resources/mapper";
+
+ /** 默认上级菜单,系统工具 */
+ private static final String DEFAULT_PARENT_MENU_ID = "3";
+
+ /**
+ * 设置模板变量信息
+ *
+ * @return 模板列表
+ */
+ public static VelocityContext prepareContext(GenTable genTable)
+ {
+ String moduleName = genTable.getModuleName();
+ String businessName = genTable.getBusinessName();
+ String packageName = genTable.getPackageName();
+ String tplCategory = genTable.getTplCategory();
+ String functionName = genTable.getFunctionName();
+
+ VelocityContext velocityContext = new VelocityContext();
+ velocityContext.put("tplCategory", genTable.getTplCategory());
+ velocityContext.put("tableName", genTable.getTableName());
+ velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
+ velocityContext.put("ClassName", genTable.getClassName());
+ velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
+ velocityContext.put("moduleName", genTable.getModuleName());
+ velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
+ velocityContext.put("businessName", genTable.getBusinessName());
+ velocityContext.put("basePackage", getPackagePrefix(packageName));
+ velocityContext.put("packageName", packageName);
+ velocityContext.put("author", genTable.getFunctionAuthor());
+ velocityContext.put("datetime", DateUtils.getDate());
+ velocityContext.put("pkColumn", genTable.getPkColumn());
+ velocityContext.put("importList", getImportList(genTable));
+ velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
+ velocityContext.put("columns", genTable.getColumns());
+ velocityContext.put("table", genTable);
+ velocityContext.put("dicts", getDicts(genTable));
+ setMenuVelocityContext(velocityContext, genTable);
+ if (GenConstants.TPL_TREE.equals(tplCategory))
+ {
+ setTreeVelocityContext(velocityContext, genTable);
+ }
+ if (GenConstants.TPL_SUB.equals(tplCategory))
+ {
+ setSubVelocityContext(velocityContext, genTable);
+ }
+ return velocityContext;
+ }
+
+ public static void setMenuVelocityContext(VelocityContext context, GenTable genTable)
+ {
+ String options = genTable.getOptions();
+ JSONObject paramsObj = JSON.parseObject(options);
+ String parentMenuId = getParentMenuId(paramsObj);
+ context.put("parentMenuId", parentMenuId);
+ }
+
+ public static void setTreeVelocityContext(VelocityContext context, GenTable genTable)
+ {
+ String options = genTable.getOptions();
+ JSONObject paramsObj = JSON.parseObject(options);
+ String treeCode = getTreecode(paramsObj);
+ String treeParentCode = getTreeParentCode(paramsObj);
+ String treeName = getTreeName(paramsObj);
+
+ context.put("treeCode", treeCode);
+ context.put("treeParentCode", treeParentCode);
+ context.put("treeName", treeName);
+ context.put("expandColumn", getExpandColumn(genTable));
+ if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE))
+ {
+ context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE));
+ }
+ if (paramsObj.containsKey(GenConstants.TREE_NAME))
+ {
+ context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME));
+ }
+ }
+
+ public static void setSubVelocityContext(VelocityContext context, GenTable genTable)
+ {
+ GenTable subTable = genTable.getSubTable();
+ String subTableName = genTable.getSubTableName();
+ String subTableFkName = genTable.getSubTableFkName();
+ String subClassName = genTable.getSubTable().getClassName();
+ String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName);
+
+ context.put("subTable", subTable);
+ context.put("subTableName", subTableName);
+ context.put("subTableFkName", subTableFkName);
+ context.put("subTableFkClassName", subTableFkClassName);
+ context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName));
+ context.put("subClassName", subClassName);
+ context.put("subclassName", StringUtils.uncapitalize(subClassName));
+ context.put("subImportList", getImportList(genTable.getSubTable()));
+ }
+
+ /**
+ * 获取模板信息
+ * @param tplCategory 生成的模板
+ * @param tplWebType 前端类型
+ * @return 模板列表
+ */
+ public static List<String> getTemplateList(String tplCategory, String tplWebType)
+ {
+ String useWebType = "vm/vue";
+ if ("element-plus".equals(tplWebType))
+ {
+ useWebType = "vm/vue/v3";
+ }
+ List<String> templates = new ArrayList<String>();
+ templates.add("vm/java/domain.java.vm");
+ templates.add("vm/java/mapper.java.vm");
+ templates.add("vm/java/service.java.vm");
+ templates.add("vm/java/serviceImpl.java.vm");
+ templates.add("vm/java/controller.java.vm");
+ templates.add("vm/xml/mapper.xml.vm");
+ templates.add("vm/sql/sql.vm");
+ templates.add("vm/js/api.js.vm");
+ if (GenConstants.TPL_CRUD.equals(tplCategory))
+ {
+ templates.add(useWebType + "/index.vue.vm");
+ }
+ else if (GenConstants.TPL_TREE.equals(tplCategory))
+ {
+ templates.add(useWebType + "/index-tree.vue.vm");
+ }
+ else if (GenConstants.TPL_SUB.equals(tplCategory))
+ {
+ templates.add(useWebType + "/index.vue.vm");
+ templates.add("vm/java/sub-domain.java.vm");
+ }
+ return templates;
+ }
+
+ /**
+ * 获取文件名
+ */
+ public static String getFileName(String template, GenTable genTable)
+ {
+ // 文件名称
+ String fileName = "";
+ // 包路径
+ String packageName = genTable.getPackageName();
+ // 模块名
+ String moduleName = genTable.getModuleName();
+ // 大写类名
+ String className = genTable.getClassName();
+ // 业务名称
+ String businessName = genTable.getBusinessName();
+
+ String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
+ String mybatisPath = MYBATIS_PATH + "/" + moduleName;
+ String vuePath = "vue";
+
+ if (template.contains("domain.java.vm"))
+ {
+ fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
+ }
+ if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory()))
+ {
+ fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName());
+ }
+ else if (template.contains("mapper.java.vm"))
+ {
+ fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className);
+ }
+ else if (template.contains("service.java.vm"))
+ {
+ fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className);
+ }
+ else if (template.contains("serviceImpl.java.vm"))
+ {
+ fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className);
+ }
+ else if (template.contains("controller.java.vm"))
+ {
+ fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className);
+ }
+ else if (template.contains("mapper.xml.vm"))
+ {
+ fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className);
+ }
+ else if (template.contains("sql.vm"))
+ {
+ fileName = businessName + "Menu.sql";
+ }
+ else if (template.contains("api.js.vm"))
+ {
+ fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName);
+ }
+ else if (template.contains("index.vue.vm"))
+ {
+ fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName);
+ }
+ else if (template.contains("index-tree.vue.vm"))
+ {
+ fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName);
+ }
+ return fileName;
+ }
+
+ /**
+ * 获取包前缀
+ *
+ * @param packageName 包名称
+ * @return 包前缀名称
+ */
+ public static String getPackagePrefix(String packageName)
+ {
+ int lastIndex = packageName.lastIndexOf(".");
+ return StringUtils.substring(packageName, 0, lastIndex);
+ }
+
+ /**
+ * 根据列类型获取导入包
+ *
+ * @param genTable 业务表对象
+ * @return 返回需要导入的包列表
+ */
+ public static HashSet<String> getImportList(GenTable genTable)
+ {
+ List<GenTableColumn> columns = genTable.getColumns();
+ GenTable subGenTable = genTable.getSubTable();
+ HashSet<String> importList = new HashSet<String>();
+ if (StringUtils.isNotNull(subGenTable))
+ {
+ importList.add("java.util.List");
+ }
+ for (GenTableColumn column : columns)
+ {
+ if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType()))
+ {
+ importList.add("java.util.Date");
+ importList.add("com.fasterxml.jackson.annotation.JsonFormat");
+ }
+ else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType()))
+ {
+ importList.add("java.math.BigDecimal");
+ }
+ }
+ return importList;
+ }
+
+ /**
+ * 根据列类型获取字典组
+ *
+ * @param genTable 业务表对象
+ * @return 返回字典组
+ */
+ public static String getDicts(GenTable genTable)
+ {
+ List<GenTableColumn> columns = genTable.getColumns();
+ Set<String> dicts = new HashSet<String>();
+ addDicts(dicts, columns);
+ if (StringUtils.isNotNull(genTable.getSubTable()))
+ {
+ List<GenTableColumn> subColumns = genTable.getSubTable().getColumns();
+ addDicts(dicts, subColumns);
+ }
+ return StringUtils.join(dicts, ", ");
+ }
+
+ /**
+ * 添加字典列表
+ *
+ * @param dicts 字典列表
+ * @param columns 列集合
+ */
+ public static void addDicts(Set<String> dicts, List<GenTableColumn> columns)
+ {
+ for (GenTableColumn column : columns)
+ {
+ if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny(
+ column.getHtmlType(),
+ new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX }))
+ {
+ dicts.add("'" + column.getDictType() + "'");
+ }
+ }
+ }
+
+ /**
+ * 获取权限前缀
+ *
+ * @param moduleName 模块名称
+ * @param businessName 业务名称
+ * @return 返回权限前缀
+ */
+ public static String getPermissionPrefix(String moduleName, String businessName)
+ {
+ return StringUtils.format("{}:{}", moduleName, businessName);
+ }
+
+ /**
+ * 获取上级菜单ID字段
+ *
+ * @param paramsObj 生成其他选项
+ * @return 上级菜单ID字段
+ */
+ public static String getParentMenuId(JSONObject paramsObj)
+ {
+ if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)
+ && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID)))
+ {
+ return paramsObj.getString(GenConstants.PARENT_MENU_ID);
+ }
+ return DEFAULT_PARENT_MENU_ID;
+ }
+
+ /**
+ * 获取树编码
+ *
+ * @param paramsObj 生成其他选项
+ * @return 树编码
+ */
+ public static String getTreecode(JSONObject paramsObj)
+ {
+ if (paramsObj.containsKey(GenConstants.TREE_CODE))
+ {
+ return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE));
+ }
+ return StringUtils.EMPTY;
+ }
+
+ /**
+ * 获取树父编码
+ *
+ * @param paramsObj 生成其他选项
+ * @return 树父编码
+ */
+ public static String getTreeParentCode(JSONObject paramsObj)
+ {
+ if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE))
+ {
+ return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE));
+ }
+ return StringUtils.EMPTY;
+ }
+
+ /**
+ * 获取树名称
+ *
+ * @param paramsObj 生成其他选项
+ * @return 树名称
+ */
+ public static String getTreeName(JSONObject paramsObj)
+ {
+ if (paramsObj.containsKey(GenConstants.TREE_NAME))
+ {
+ return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME));
+ }
+ return StringUtils.EMPTY;
+ }
+
+ /**
+ * 获取需要在哪一列上面显示展开按钮
+ *
+ * @param genTable 业务表对象
+ * @return 展开按钮列序号
+ */
+ public static int getExpandColumn(GenTable genTable)
+ {
+ String options = genTable.getOptions();
+ JSONObject paramsObj = JSON.parseObject(options);
+ String treeName = paramsObj.getString(GenConstants.TREE_NAME);
+ int num = 0;
+ for (GenTableColumn column : genTable.getColumns())
+ {
+ if (column.isList())
+ {
+ num++;
+ String columnName = column.getColumnName();
+ if (columnName.equals(treeName))
+ {
+ break;
+ }
+ }
+ }
+ return num;
+ }
+}
diff --git a/ruoyi-generator/src/main/resources/generator.yml b/ruoyi-generator/src/main/resources/generator.yml
new file mode 100644
index 0000000..7eae68e
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/generator.yml
@@ -0,0 +1,10 @@
+# 代码生成
+gen:
+ # 作者
+ author: ruoyi
+ # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
+ packageName: com.ruoyi.system
+ # 自动去除表前缀,默认是false
+ autoRemovePre: false
+ # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
+ tablePrefix: sys_
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml
new file mode 100644
index 0000000..52857e8
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.generator.mapper.GenTableColumnMapper">
+
+ <resultMap type="GenTableColumn" id="GenTableColumnResult">
+ <id property="columnId" column="column_id" />
+ <result property="tableId" column="table_id" />
+ <result property="columnName" column="column_name" />
+ <result property="columnComment" column="column_comment" />
+ <result property="columnType" column="column_type" />
+ <result property="javaType" column="java_type" />
+ <result property="javaField" column="java_field" />
+ <result property="isPk" column="is_pk" />
+ <result property="isIncrement" column="is_increment" />
+ <result property="isRequired" column="is_required" />
+ <result property="isInsert" column="is_insert" />
+ <result property="isEdit" column="is_edit" />
+ <result property="isList" column="is_list" />
+ <result property="isQuery" column="is_query" />
+ <result property="queryType" column="query_type" />
+ <result property="htmlType" column="html_type" />
+ <result property="dictType" column="dict_type" />
+ <result property="sort" column="sort" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectGenTableColumnVo">
+ select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column
+ </sql>
+
+ <select id="selectGenTableColumnListByTableId" parameterType="Long" resultMap="GenTableColumnResult">
+ <include refid="selectGenTableColumnVo"/>
+ where table_id = #{tableId}
+ order by sort
+ </select>
+
+ <select id="selectDbTableColumnsByName" parameterType="String" resultMap="GenTableColumnResult">
+ select column_name, (case when (is_nullable = 'no' <![CDATA[ && ]]> column_key != 'PRI') then '1' else '0' end) as is_required, (case when column_key = 'PRI' then '1' else '0' end) as is_pk, ordinal_position as sort, column_comment, (case when extra = 'auto_increment' then '1' else '0' end) as is_increment, column_type
+ from information_schema.columns where table_schema = (select database()) and table_name = (#{tableName})
+ order by ordinal_position
+ </select>
+
+ <insert id="insertGenTableColumn" parameterType="GenTableColumn" useGeneratedKeys="true" keyProperty="columnId">
+ insert into gen_table_column (
+ <if test="tableId != null and tableId != ''">table_id,</if>
+ <if test="columnName != null and columnName != ''">column_name,</if>
+ <if test="columnComment != null and columnComment != ''">column_comment,</if>
+ <if test="columnType != null and columnType != ''">column_type,</if>
+ <if test="javaType != null and javaType != ''">java_type,</if>
+ <if test="javaField != null and javaField != ''">java_field,</if>
+ <if test="isPk != null and isPk != ''">is_pk,</if>
+ <if test="isIncrement != null and isIncrement != ''">is_increment,</if>
+ <if test="isRequired != null and isRequired != ''">is_required,</if>
+ <if test="isInsert != null and isInsert != ''">is_insert,</if>
+ <if test="isEdit != null and isEdit != ''">is_edit,</if>
+ <if test="isList != null and isList != ''">is_list,</if>
+ <if test="isQuery != null and isQuery != ''">is_query,</if>
+ <if test="queryType != null and queryType != ''">query_type,</if>
+ <if test="htmlType != null and htmlType != ''">html_type,</if>
+ <if test="dictType != null and dictType != ''">dict_type,</if>
+ <if test="sort != null">sort,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="tableId != null and tableId != ''">#{tableId},</if>
+ <if test="columnName != null and columnName != ''">#{columnName},</if>
+ <if test="columnComment != null and columnComment != ''">#{columnComment},</if>
+ <if test="columnType != null and columnType != ''">#{columnType},</if>
+ <if test="javaType != null and javaType != ''">#{javaType},</if>
+ <if test="javaField != null and javaField != ''">#{javaField},</if>
+ <if test="isPk != null and isPk != ''">#{isPk},</if>
+ <if test="isIncrement != null and isIncrement != ''">#{isIncrement},</if>
+ <if test="isRequired != null and isRequired != ''">#{isRequired},</if>
+ <if test="isInsert != null and isInsert != ''">#{isInsert},</if>
+ <if test="isEdit != null and isEdit != ''">#{isEdit},</if>
+ <if test="isList != null and isList != ''">#{isList},</if>
+ <if test="isQuery != null and isQuery != ''">#{isQuery},</if>
+ <if test="queryType != null and queryType != ''">#{queryType},</if>
+ <if test="htmlType != null and htmlType != ''">#{htmlType},</if>
+ <if test="dictType != null and dictType != ''">#{dictType},</if>
+ <if test="sort != null">#{sort},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateGenTableColumn" parameterType="GenTableColumn">
+ update gen_table_column
+ <set>
+ <if test="columnComment != null">column_comment = #{columnComment},</if>
+ <if test="javaType != null">java_type = #{javaType},</if>
+ <if test="javaField != null">java_field = #{javaField},</if>
+ <if test="isInsert != null">is_insert = #{isInsert},</if>
+ <if test="isEdit != null">is_edit = #{isEdit},</if>
+ <if test="isList != null">is_list = #{isList},</if>
+ <if test="isQuery != null">is_query = #{isQuery},</if>
+ <if test="isRequired != null">is_required = #{isRequired},</if>
+ <if test="queryType != null">query_type = #{queryType},</if>
+ <if test="htmlType != null">html_type = #{htmlType},</if>
+ <if test="dictType != null">dict_type = #{dictType},</if>
+ <if test="sort != null">sort = #{sort},</if>
+ <if test="updateBy != null">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where column_id = #{columnId}
+ </update>
+
+ <delete id="deleteGenTableColumnByIds" parameterType="Long">
+ delete from gen_table_column where table_id in
+ <foreach collection="array" item="tableId" open="(" separator="," close=")">
+ #{tableId}
+ </foreach>
+ </delete>
+
+ <delete id="deleteGenTableColumns">
+ delete from gen_table_column where column_id in
+ <foreach collection="list" item="item" open="(" separator="," close=")">
+ #{item.columnId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml
new file mode 100644
index 0000000..d1110f7
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml
@@ -0,0 +1,210 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.generator.mapper.GenTableMapper">
+
+ <resultMap type="GenTable" id="GenTableResult">
+ <id property="tableId" column="table_id" />
+ <result property="tableName" column="table_name" />
+ <result property="tableComment" column="table_comment" />
+ <result property="subTableName" column="sub_table_name" />
+ <result property="subTableFkName" column="sub_table_fk_name" />
+ <result property="className" column="class_name" />
+ <result property="tplCategory" column="tpl_category" />
+ <result property="tplWebType" column="tpl_web_type" />
+ <result property="packageName" column="package_name" />
+ <result property="moduleName" column="module_name" />
+ <result property="businessName" column="business_name" />
+ <result property="functionName" column="function_name" />
+ <result property="functionAuthor" column="function_author" />
+ <result property="genType" column="gen_type" />
+ <result property="genPath" column="gen_path" />
+ <result property="options" column="options" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ <collection property="columns" javaType="java.util.List" resultMap="GenTableColumnResult" />
+ </resultMap>
+
+ <resultMap type="GenTableColumn" id="GenTableColumnResult">
+ <id property="columnId" column="column_id" />
+ <result property="tableId" column="table_id" />
+ <result property="columnName" column="column_name" />
+ <result property="columnComment" column="column_comment" />
+ <result property="columnType" column="column_type" />
+ <result property="javaType" column="java_type" />
+ <result property="javaField" column="java_field" />
+ <result property="isPk" column="is_pk" />
+ <result property="isIncrement" column="is_increment" />
+ <result property="isRequired" column="is_required" />
+ <result property="isInsert" column="is_insert" />
+ <result property="isEdit" column="is_edit" />
+ <result property="isList" column="is_list" />
+ <result property="isQuery" column="is_query" />
+ <result property="queryType" column="query_type" />
+ <result property="htmlType" column="html_type" />
+ <result property="dictType" column="dict_type" />
+ <result property="sort" column="sort" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectGenTableVo">
+ select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table
+ </sql>
+
+ <select id="selectGenTableList" parameterType="GenTable" resultMap="GenTableResult">
+ <include refid="selectGenTableVo"/>
+ <where>
+ <if test="tableName != null and tableName != ''">
+ AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
+ </if>
+ <if test="tableComment != null and tableComment != ''">
+ AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ AND date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ AND date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ </where>
+ </select>
+
+ <select id="selectDbTableList" parameterType="GenTable" resultMap="GenTableResult">
+ select table_name, table_comment, create_time, update_time from information_schema.tables
+ where table_schema = (select database())
+ AND table_name NOT LIKE 'qrtz\_%' AND table_name NOT LIKE 'gen\_%'
+ AND table_name NOT IN (select table_name from gen_table)
+ <if test="tableName != null and tableName != ''">
+ AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
+ </if>
+ <if test="tableComment != null and tableComment != ''">
+ AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ AND date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ AND date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ order by create_time desc
+ </select>
+
+ <select id="selectDbTableListByNames" resultMap="GenTableResult">
+ select table_name, table_comment, create_time, update_time from information_schema.tables
+ where table_name NOT LIKE 'qrtz\_%' and table_name NOT LIKE 'gen\_%' and table_schema = (select database())
+ and table_name in
+ <foreach collection="array" item="name" open="(" separator="," close=")">
+ #{name}
+ </foreach>
+ </select>
+
+ <select id="selectTableByName" parameterType="String" resultMap="GenTableResult">
+ select table_name, table_comment, create_time, update_time from information_schema.tables
+ where table_comment <![CDATA[ <> ]]> '' and table_schema = (select database())
+ and table_name = #{tableName}
+ </select>
+
+ <select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
+ SELECT t.table_id, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.tpl_web_type, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
+ c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
+ FROM gen_table t
+ LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+ where t.table_id = #{tableId} order by c.sort
+ </select>
+
+ <select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
+ SELECT t.table_id, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.tpl_web_type, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
+ c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
+ FROM gen_table t
+ LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+ where t.table_name = #{tableName} order by c.sort
+ </select>
+
+ <select id="selectGenTableAll" parameterType="String" resultMap="GenTableResult">
+ SELECT t.table_id, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.tpl_web_type, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.options, t.remark,
+ c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
+ FROM gen_table t
+ LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+ order by c.sort
+ </select>
+
+ <insert id="insertGenTable" parameterType="GenTable" useGeneratedKeys="true" keyProperty="tableId">
+ insert into gen_table (
+ <if test="tableName != null">table_name,</if>
+ <if test="tableComment != null and tableComment != ''">table_comment,</if>
+ <if test="className != null and className != ''">class_name,</if>
+ <if test="tplCategory != null and tplCategory != ''">tpl_category,</if>
+ <if test="tplWebType != null and tplWebType != ''">tpl_web_type,</if>
+ <if test="packageName != null and packageName != ''">package_name,</if>
+ <if test="moduleName != null and moduleName != ''">module_name,</if>
+ <if test="businessName != null and businessName != ''">business_name,</if>
+ <if test="functionName != null and functionName != ''">function_name,</if>
+ <if test="functionAuthor != null and functionAuthor != ''">function_author,</if>
+ <if test="genType != null and genType != ''">gen_type,</if>
+ <if test="genPath != null and genPath != ''">gen_path,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="tableName != null">#{tableName},</if>
+ <if test="tableComment != null and tableComment != ''">#{tableComment},</if>
+ <if test="className != null and className != ''">#{className},</if>
+ <if test="tplCategory != null and tplCategory != ''">#{tplCategory},</if>
+ <if test="tplWebType != null and tplWebType != ''">#{tplWebType},</if>
+ <if test="packageName != null and packageName != ''">#{packageName},</if>
+ <if test="moduleName != null and moduleName != ''">#{moduleName},</if>
+ <if test="businessName != null and businessName != ''">#{businessName},</if>
+ <if test="functionName != null and functionName != ''">#{functionName},</if>
+ <if test="functionAuthor != null and functionAuthor != ''">#{functionAuthor},</if>
+ <if test="genType != null and genType != ''">#{genType},</if>
+ <if test="genPath != null and genPath != ''">#{genPath},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="createTable">
+ ${sql}
+ </update>
+
+ <update id="updateGenTable" parameterType="GenTable">
+ update gen_table
+ <set>
+ <if test="tableName != null">table_name = #{tableName},</if>
+ <if test="tableComment != null and tableComment != ''">table_comment = #{tableComment},</if>
+ <if test="subTableName != null">sub_table_name = #{subTableName},</if>
+ <if test="subTableFkName != null">sub_table_fk_name = #{subTableFkName},</if>
+ <if test="className != null and className != ''">class_name = #{className},</if>
+ <if test="functionAuthor != null and functionAuthor != ''">function_author = #{functionAuthor},</if>
+ <if test="genType != null and genType != ''">gen_type = #{genType},</if>
+ <if test="genPath != null and genPath != ''">gen_path = #{genPath},</if>
+ <if test="tplCategory != null and tplCategory != ''">tpl_category = #{tplCategory},</if>
+ <if test="tplWebType != null and tplWebType != ''">tpl_web_type = #{tplWebType},</if>
+ <if test="packageName != null and packageName != ''">package_name = #{packageName},</if>
+ <if test="moduleName != null and moduleName != ''">module_name = #{moduleName},</if>
+ <if test="businessName != null and businessName != ''">business_name = #{businessName},</if>
+ <if test="functionName != null and functionName != ''">function_name = #{functionName},</if>
+ <if test="options != null and options != ''">options = #{options},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ update_time = sysdate()
+ </set>
+ where table_id = #{tableId}
+ </update>
+
+ <delete id="deleteGenTableByIds" parameterType="Long">
+ delete from gen_table where table_id in
+ <foreach collection="array" item="tableId" open="(" separator="," close=")">
+ #{tableId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm
new file mode 100644
index 0000000..b8457a3
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm
@@ -0,0 +1,115 @@
+package ${packageName}.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import ${packageName}.domain.${ClassName};
+import ${packageName}.service.I${ClassName}Service;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+#if($table.crud || $table.sub)
+import com.ruoyi.common.core.page.TableDataInfo;
+#elseif($table.tree)
+#end
+
+/**
+ * ${functionName}Controller
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+@RestController
+@RequestMapping("/${moduleName}/${businessName}")
+public class ${ClassName}Controller extends BaseController
+{
+ @Autowired
+ private I${ClassName}Service ${className}Service;
+
+ /**
+ * 查询${functionName}列表
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')")
+ @GetMapping("/list")
+#if($table.crud || $table.sub)
+ public TableDataInfo list(${ClassName} ${className})
+ {
+ startPage();
+ List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
+ return getDataTable(list);
+ }
+#elseif($table.tree)
+ public AjaxResult list(${ClassName} ${className})
+ {
+ List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
+ return success(list);
+ }
+#end
+
+ /**
+ * 导出${functionName}列表
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')")
+ @Log(title = "${functionName}", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, ${ClassName} ${className})
+ {
+ List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
+ ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class);
+ util.exportExcel(response, list, "${functionName}数据");
+ }
+
+ /**
+ * 获取${functionName}详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')")
+ @GetMapping(value = "/{${pkColumn.javaField}}")
+ public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField})
+ {
+ return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}));
+ }
+
+ /**
+ * 新增${functionName}
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')")
+ @Log(title = "${functionName}", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody ${ClassName} ${className})
+ {
+ return toAjax(${className}Service.insert${ClassName}(${className}));
+ }
+
+ /**
+ * 修改${functionName}
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')")
+ @Log(title = "${functionName}", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody ${ClassName} ${className})
+ {
+ return toAjax(${className}Service.update${ClassName}(${className}));
+ }
+
+ /**
+ * 删除${functionName}
+ */
+ @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')")
+ @Log(title = "${functionName}", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{${pkColumn.javaField}s}")
+ public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s)
+ {
+ return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s));
+ }
+}
diff --git a/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm
new file mode 100644
index 0000000..bd51c17
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm
@@ -0,0 +1,105 @@
+package ${packageName}.domain;
+
+#foreach ($import in $importList)
+import ${import};
+#end
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+#if($table.crud || $table.sub)
+import com.ruoyi.common.core.domain.BaseEntity;
+#elseif($table.tree)
+import com.ruoyi.common.core.domain.TreeEntity;
+#end
+
+/**
+ * ${functionName}对象 ${tableName}
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+#if($table.crud || $table.sub)
+#set($Entity="BaseEntity")
+#elseif($table.tree)
+#set($Entity="TreeEntity")
+#end
+public class ${ClassName} extends ${Entity}
+{
+ private static final long serialVersionUID = 1L;
+
+#foreach ($column in $columns)
+#if(!$table.isSuperColumn($column.javaField))
+ /** $column.columnComment */
+#if($column.list)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($parentheseIndex != -1)
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+#elseif($column.javaType == 'Date')
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
+#else
+ @Excel(name = "${comment}")
+#end
+#end
+ private $column.javaType $column.javaField;
+
+#end
+#end
+#if($table.sub)
+ /** $table.subTable.functionName信息 */
+ private List<${subClassName}> ${subclassName}List;
+
+#end
+#foreach ($column in $columns)
+#if(!$table.isSuperColumn($column.javaField))
+#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
+#set($AttrName=$column.javaField)
+#else
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#end
+ public void set${AttrName}($column.javaType $column.javaField)
+ {
+ this.$column.javaField = $column.javaField;
+ }
+
+ public $column.javaType get${AttrName}()
+ {
+ return $column.javaField;
+ }
+#end
+#end
+
+#if($table.sub)
+ public List<${subClassName}> get${subClassName}List()
+ {
+ return ${subclassName}List;
+ }
+
+ public void set${subClassName}List(List<${subClassName}> ${subclassName}List)
+ {
+ this.${subclassName}List = ${subclassName}List;
+ }
+
+#end
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+#foreach ($column in $columns)
+#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
+#set($AttrName=$column.javaField)
+#else
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#end
+ .append("${column.javaField}", get${AttrName}())
+#end
+#if($table.sub)
+ .append("${subclassName}List", get${subClassName}List())
+#end
+ .toString();
+ }
+}
diff --git a/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm
new file mode 100644
index 0000000..7e7d7c2
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm
@@ -0,0 +1,91 @@
+package ${packageName}.mapper;
+
+import java.util.List;
+import ${packageName}.domain.${ClassName};
+#if($table.sub)
+import ${packageName}.domain.${subClassName};
+#end
+
+/**
+ * ${functionName}Mapper接口
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+public interface ${ClassName}Mapper
+{
+ /**
+ * 查询${functionName}
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return ${functionName}
+ */
+ public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
+
+ /**
+ * 查询${functionName}列表
+ *
+ * @param ${className} ${functionName}
+ * @return ${functionName}集合
+ */
+ public List<${ClassName}> select${ClassName}List(${ClassName} ${className});
+
+ /**
+ * 新增${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+ public int insert${ClassName}(${ClassName} ${className});
+
+ /**
+ * 修改${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+ public int update${ClassName}(${ClassName} ${className});
+
+ /**
+ * 删除${functionName}
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return 结果
+ */
+ public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
+
+ /**
+ * 批量删除${functionName}
+ *
+ * @param ${pkColumn.javaField}s 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
+#if($table.sub)
+
+ /**
+ * 批量删除${subTable.functionName}
+ *
+ * @param ${pkColumn.javaField}s 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
+
+ /**
+ * 批量新增${subTable.functionName}
+ *
+ * @param ${subclassName}List ${subTable.functionName}列表
+ * @return 结果
+ */
+ public int batch${subClassName}(List<${subClassName}> ${subclassName}List);
+
+
+ /**
+ * 通过${functionName}主键删除${subTable.functionName}信息
+ *
+ * @param ${pkColumn.javaField} ${functionName}ID
+ * @return 结果
+ */
+ public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField});
+#end
+}
diff --git a/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ruoyi-generator/src/main/resources/vm/java/service.java.vm
new file mode 100644
index 0000000..264882b
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/service.java.vm
@@ -0,0 +1,61 @@
+package ${packageName}.service;
+
+import java.util.List;
+import ${packageName}.domain.${ClassName};
+
+/**
+ * ${functionName}Service接口
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+public interface I${ClassName}Service
+{
+ /**
+ * 查询${functionName}
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return ${functionName}
+ */
+ public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
+
+ /**
+ * 查询${functionName}列表
+ *
+ * @param ${className} ${functionName}
+ * @return ${functionName}集合
+ */
+ public List<${ClassName}> select${ClassName}List(${ClassName} ${className});
+
+ /**
+ * 新增${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+ public int insert${ClassName}(${ClassName} ${className});
+
+ /**
+ * 修改${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+ public int update${ClassName}(${ClassName} ${className});
+
+ /**
+ * 批量删除${functionName}
+ *
+ * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合
+ * @return 结果
+ */
+ public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
+
+ /**
+ * 删除${functionName}信息
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return 结果
+ */
+ public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
+}
diff --git a/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm
new file mode 100644
index 0000000..14746e1
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm
@@ -0,0 +1,169 @@
+package ${packageName}.service.impl;
+
+import java.util.List;
+#foreach ($column in $columns)
+#if($column.javaField == 'createTime' || $column.javaField == 'updateTime')
+import com.ruoyi.common.utils.DateUtils;
+#break
+#end
+#end
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+#if($table.sub)
+import java.util.ArrayList;
+import com.ruoyi.common.utils.StringUtils;
+import org.springframework.transaction.annotation.Transactional;
+import ${packageName}.domain.${subClassName};
+#end
+import ${packageName}.mapper.${ClassName}Mapper;
+import ${packageName}.domain.${ClassName};
+import ${packageName}.service.I${ClassName}Service;
+
+/**
+ * ${functionName}Service业务层处理
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+@Service
+public class ${ClassName}ServiceImpl implements I${ClassName}Service
+{
+ @Autowired
+ private ${ClassName}Mapper ${className}Mapper;
+
+ /**
+ * 查询${functionName}
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return ${functionName}
+ */
+ @Override
+ public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
+ {
+ return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
+ }
+
+ /**
+ * 查询${functionName}列表
+ *
+ * @param ${className} ${functionName}
+ * @return ${functionName}
+ */
+ @Override
+ public List<${ClassName}> select${ClassName}List(${ClassName} ${className})
+ {
+ return ${className}Mapper.select${ClassName}List(${className});
+ }
+
+ /**
+ * 新增${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+#if($table.sub)
+ @Transactional
+#end
+ @Override
+ public int insert${ClassName}(${ClassName} ${className})
+ {
+#foreach ($column in $columns)
+#if($column.javaField == 'createTime')
+ ${className}.setCreateTime(DateUtils.getNowDate());
+#end
+#end
+#if($table.sub)
+ int rows = ${className}Mapper.insert${ClassName}(${className});
+ insert${subClassName}(${className});
+ return rows;
+#else
+ return ${className}Mapper.insert${ClassName}(${className});
+#end
+ }
+
+ /**
+ * 修改${functionName}
+ *
+ * @param ${className} ${functionName}
+ * @return 结果
+ */
+#if($table.sub)
+ @Transactional
+#end
+ @Override
+ public int update${ClassName}(${ClassName} ${className})
+ {
+#foreach ($column in $columns)
+#if($column.javaField == 'updateTime')
+ ${className}.setUpdateTime(DateUtils.getNowDate());
+#end
+#end
+#if($table.sub)
+ ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}());
+ insert${subClassName}(${className});
+#end
+ return ${className}Mapper.update${ClassName}(${className});
+ }
+
+ /**
+ * 批量删除${functionName}
+ *
+ * @param ${pkColumn.javaField}s 需要删除的${functionName}主键
+ * @return 结果
+ */
+#if($table.sub)
+ @Transactional
+#end
+ @Override
+ public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s)
+ {
+#if($table.sub)
+ ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s);
+#end
+ return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s);
+ }
+
+ /**
+ * 删除${functionName}信息
+ *
+ * @param ${pkColumn.javaField} ${functionName}主键
+ * @return 结果
+ */
+#if($table.sub)
+ @Transactional
+#end
+ @Override
+ public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
+ {
+#if($table.sub)
+ ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField});
+#end
+ return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
+ }
+#if($table.sub)
+
+ /**
+ * 新增${subTable.functionName}信息
+ *
+ * @param ${className} ${functionName}对象
+ */
+ public void insert${subClassName}(${ClassName} ${className})
+ {
+ List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List();
+ ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}();
+ if (StringUtils.isNotNull(${subclassName}List))
+ {
+ List<${subClassName}> list = new ArrayList<${subClassName}>();
+ for (${subClassName} ${subclassName} : ${subclassName}List)
+ {
+ ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField});
+ list.add(${subclassName});
+ }
+ if (list.size() > 0)
+ {
+ ${className}Mapper.batch${subClassName}(list);
+ }
+ }
+ }
+#end
+}
diff --git a/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm
new file mode 100644
index 0000000..a3f53eb
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm
@@ -0,0 +1,76 @@
+package ${packageName}.domain;
+
+#foreach ($import in $subImportList)
+import ${import};
+#end
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * ${subTable.functionName}对象 ${subTableName}
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+public class ${subClassName} extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+#foreach ($column in $subTable.columns)
+#if(!$table.isSuperColumn($column.javaField))
+ /** $column.columnComment */
+#if($column.list)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($parentheseIndex != -1)
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+#elseif($column.javaType == 'Date')
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
+#else
+ @Excel(name = "${comment}")
+#end
+#end
+ private $column.javaType $column.javaField;
+
+#end
+#end
+#foreach ($column in $subTable.columns)
+#if(!$table.isSuperColumn($column.javaField))
+#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
+#set($AttrName=$column.javaField)
+#else
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#end
+ public void set${AttrName}($column.javaType $column.javaField)
+ {
+ this.$column.javaField = $column.javaField;
+ }
+
+ public $column.javaType get${AttrName}()
+ {
+ return $column.javaField;
+ }
+#end
+#end
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+#foreach ($column in $subTable.columns)
+#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]"))
+#set($AttrName=$column.javaField)
+#else
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#end
+ .append("${column.javaField}", get${AttrName}())
+#end
+ .toString();
+ }
+}
diff --git a/ruoyi-generator/src/main/resources/vm/js/api.js.vm b/ruoyi-generator/src/main/resources/vm/js/api.js.vm
new file mode 100644
index 0000000..9295524
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/js/api.js.vm
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询${functionName}列表
+export function list${BusinessName}(query) {
+ return request({
+ url: '/${moduleName}/${businessName}/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询${functionName}详细
+export function get${BusinessName}(${pkColumn.javaField}) {
+ return request({
+ url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
+ method: 'get'
+ })
+}
+
+// 新增${functionName}
+export function add${BusinessName}(data) {
+ return request({
+ url: '/${moduleName}/${businessName}',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改${functionName}
+export function update${BusinessName}(data) {
+ return request({
+ url: '/${moduleName}/${businessName}',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除${functionName}
+export function del${BusinessName}(${pkColumn.javaField}) {
+ return request({
+ url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-generator/src/main/resources/vm/sql/sql.vm b/ruoyi-generator/src/main/resources/vm/sql/sql.vm
new file mode 100644
index 0000000..0575583
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/sql/sql.vm
@@ -0,0 +1,22 @@
+-- 菜单 SQL
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单');
+
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, '');
\ No newline at end of file
diff --git a/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
new file mode 100644
index 0000000..4819c2a
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
@@ -0,0 +1,505 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+#foreach($column in $columns)
+#if($column.query)
+#set($dictType=$column.dictType)
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-input
+ v-model="queryParams.${column.javaField}"
+ placeholder="请输入${comment}"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-date-picker clearable
+ v-model="queryParams.${column.javaField}"
+ type="date"
+ value-format="yyyy-MM-dd"
+ placeholder="选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ <el-form-item label="${comment}">
+ <el-date-picker
+ v-model="daterange${AttrName}"
+ style="width: 240px"
+ value-format="yyyy-MM-dd"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+#end
+#end
+#end
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="el-icon-plus"
+ size="mini"
+ @click="handleAdd"
+ v-hasPermi="['${moduleName}:${businessName}:add']"
+ >新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="el-icon-sort"
+ size="mini"
+ @click="toggleExpandAll"
+ >展开/折叠</el-button>
+ </el-col>
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table
+ v-if="refreshTable"
+ v-loading="loading"
+ :data="${businessName}List"
+ row-key="${treeCode}"
+ :default-expand-all="isExpandAll"
+ :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+ >
+#foreach($column in $columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk)
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+ <template slot-scope="scope">
+ <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+ <template slot-scope="scope">
+ <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $column.dictType)
+ <el-table-column label="${comment}" align="center" prop="${javaField}">
+ <template slot-scope="scope">
+#if($column.htmlType == "checkbox")
+ <dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
+#else
+ <dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField}"/>
+#end
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $javaField)
+#if(${foreach.index} == 1)
+ <el-table-column label="${comment}" prop="${javaField}" />
+#else
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#end
+#end
+#end
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template slot-scope="scope">
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-edit"
+ @click="handleUpdate(scope.row)"
+ v-hasPermi="['${moduleName}:${businessName}:edit']"
+ >修改</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-plus"
+ @click="handleAdd(scope.row)"
+ v-hasPermi="['${moduleName}:${businessName}:add']"
+ >新增</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-delete"
+ @click="handleDelete(scope.row)"
+ v-hasPermi="['${moduleName}:${businessName}:remove']"
+ >删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 添加或修改${functionName}对话框 -->
+ <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+ <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+#foreach($column in $columns)
+#set($field=$column.javaField)
+#if($column.insert && !$column.pk)
+#if(($column.usableColumn) || (!$column.superColumn))
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#set($dictType=$column.dictType)
+#if("" != $treeParentCode && $column.javaField == $treeParentCode)
+ <el-form-item label="${comment}" prop="${treeParentCode}">
+ <treeselect v-model="form.${treeParentCode}" :options="${businessName}Options" :normalizer="normalizer" placeholder="请选择${comment}" />
+ </el-form-item>
+#elseif($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" placeholder="请输入${comment}" />
+ </el-form-item>
+#elseif($column.htmlType == "imageUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <image-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "fileUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <file-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "editor")
+ <el-form-item label="${comment}">
+ <editor v-model="form.${field}" :min-height="192"/>
+ </el-form-item>
+#elseif($column.htmlType == "select" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :value="parseInt(dict.value)"
+#else
+ :value="dict.value"
+#end
+ ></el-option>
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "select" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.value">
+ {{dict.label}}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox>请选择字典生成</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :label="parseInt(dict.value)"
+#else
+ :label="dict.value"
+#end
+ >{{dict.label}}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio label="1">请选择字典生成</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "datetime")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-date-picker clearable
+ v-model="form.${field}"
+ type="date"
+ value-format="yyyy-MM-dd"
+ placeholder="选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "textarea")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+#end
+#end
+#end
+#end
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+export default {
+ name: "${BusinessName}",
+#if(${dicts} != '')
+ dicts: [${dicts}],
+#end
+ components: {
+ Treeselect
+ },
+ data() {
+ return {
+ // 遮罩层
+ loading: true,
+ // 显示搜索条件
+ showSearch: true,
+ // ${functionName}表格数据
+ ${businessName}List: [],
+ // ${functionName}树选项
+ ${businessName}Options: [],
+ // 弹出层标题
+ title: "",
+ // 是否显示弹出层
+ open: false,
+ // 是否展开,默认全部展开
+ isExpandAll: true,
+ // 重新渲染表格状态
+ refreshTable: true,
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ // $comment时间范围
+ daterange${AttrName}: [],
+#end
+#end
+ // 查询参数
+ queryParams: {
+#foreach ($column in $columns)
+#if($column.query)
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ },
+ // 表单参数
+ form: {},
+ // 表单校验
+ rules: {
+#foreach ($column in $columns)
+#if($column.required)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+ $column.javaField: [
+ { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
+ ]#if($foreach.count != $columns.size()),#end
+#end
+#end
+ }
+ };
+ },
+ created() {
+ this.getList();
+ },
+ methods: {
+ /** 查询${functionName}列表 */
+ getList() {
+ this.loading = true;
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ this.queryParams.params = {};
+#break
+#end
+#end
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ if (null != this.daterange${AttrName} && '' != this.daterange${AttrName}) {
+ this.queryParams.params["begin${AttrName}"] = this.daterange${AttrName}[0];
+ this.queryParams.params["end${AttrName}"] = this.daterange${AttrName}[1];
+ }
+#end
+#end
+ list${BusinessName}(this.queryParams).then(response => {
+ this.${businessName}List = this.handleTree(response.data, "${treeCode}", "${treeParentCode}");
+ this.loading = false;
+ });
+ },
+ /** 转换${functionName}数据结构 */
+ normalizer(node) {
+ if (node.children && !node.children.length) {
+ delete node.children;
+ }
+ return {
+ id: node.${treeCode},
+ label: node.${treeName},
+ children: node.children
+ };
+ },
+ /** 查询${functionName}下拉树结构 */
+ getTreeselect() {
+ list${BusinessName}().then(response => {
+ this.${businessName}Options = [];
+ const data = { ${treeCode}: 0, ${treeName}: '顶级节点', children: [] };
+ data.children = this.handleTree(response.data, "${treeCode}", "${treeParentCode}");
+ this.${businessName}Options.push(data);
+ });
+ },
+ // 取消按钮
+ cancel() {
+ this.open = false;
+ this.reset();
+ },
+ // 表单重置
+ reset() {
+ this.form = {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ $column.javaField: []#if($foreach.count != $columns.size()),#end
+#else
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ };
+ this.resetForm("form");
+ },
+ /** 搜索按钮操作 */
+ handleQuery() {
+ this.getList();
+ },
+ /** 重置按钮操作 */
+ resetQuery() {
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ this.daterange${AttrName} = [];
+#end
+#end
+ this.resetForm("queryForm");
+ this.handleQuery();
+ },
+ /** 新增按钮操作 */
+ handleAdd(row) {
+ this.reset();
+ this.getTreeselect();
+ if (row != null && row.${treeCode}) {
+ this.form.${treeParentCode} = row.${treeCode};
+ } else {
+ this.form.${treeParentCode} = 0;
+ }
+ this.open = true;
+ this.title = "添加${functionName}";
+ },
+ /** 展开/折叠操作 */
+ toggleExpandAll() {
+ this.refreshTable = false;
+ this.isExpandAll = !this.isExpandAll;
+ this.$nextTick(() => {
+ this.refreshTable = true;
+ });
+ },
+ /** 修改按钮操作 */
+ handleUpdate(row) {
+ this.reset();
+ this.getTreeselect();
+ if (row != null) {
+ this.form.${treeParentCode} = row.${treeParentCode};
+ }
+ get${BusinessName}(row.${pkColumn.javaField}).then(response => {
+ this.form = response.data;
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ this.form.$column.javaField = this.form.${column.javaField}.split(",");
+#end
+#end
+ this.open = true;
+ this.title = "修改${functionName}";
+ });
+ },
+ /** 提交按钮 */
+ submitForm() {
+ this.#[[$]]#refs["form"].validate(valid => {
+ if (valid) {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ this.form.$column.javaField = this.form.${column.javaField}.join(",");
+#end
+#end
+ if (this.form.${pkColumn.javaField} != null) {
+ update${BusinessName}(this.form).then(response => {
+ this.#[[$modal]]#.msgSuccess("修改成功");
+ this.open = false;
+ this.getList();
+ });
+ } else {
+ add${BusinessName}(this.form).then(response => {
+ this.#[[$modal]]#.msgSuccess("新增成功");
+ this.open = false;
+ this.getList();
+ });
+ }
+ }
+ });
+ },
+ /** 删除按钮操作 */
+ handleDelete(row) {
+ this.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?').then(function() {
+ return del${BusinessName}(row.${pkColumn.javaField});
+ }).then(() => {
+ this.getList();
+ this.#[[$modal]]#.msgSuccess("删除成功");
+ }).catch(() => {});
+ }
+ }
+};
+</script>
diff --git a/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
new file mode 100644
index 0000000..6296014
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
@@ -0,0 +1,602 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+#foreach($column in $columns)
+#if($column.query)
+#set($dictType=$column.dictType)
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-input
+ v-model="queryParams.${column.javaField}"
+ placeholder="请输入${comment}"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-date-picker clearable
+ v-model="queryParams.${column.javaField}"
+ type="date"
+ value-format="yyyy-MM-dd"
+ placeholder="请选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ <el-form-item label="${comment}">
+ <el-date-picker
+ v-model="daterange${AttrName}"
+ style="width: 240px"
+ value-format="yyyy-MM-dd"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+#end
+#end
+#end
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="el-icon-plus"
+ size="mini"
+ @click="handleAdd"
+ v-hasPermi="['${moduleName}:${businessName}:add']"
+ >新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="el-icon-edit"
+ size="mini"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['${moduleName}:${businessName}:edit']"
+ >修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="el-icon-delete"
+ size="mini"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['${moduleName}:${businessName}:remove']"
+ >删除</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="el-icon-download"
+ size="mini"
+ @click="handleExport"
+ v-hasPermi="['${moduleName}:${businessName}:export']"
+ >导出</el-button>
+ </el-col>
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="${businessName}List" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+#foreach($column in $columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk)
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+ <template slot-scope="scope">
+ <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+ <template slot-scope="scope">
+ <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $column.dictType)
+ <el-table-column label="${comment}" align="center" prop="${javaField}">
+ <template slot-scope="scope">
+#if($column.htmlType == "checkbox")
+ <dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
+#else
+ <dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField}"/>
+#end
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $javaField)
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#end
+#end
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template slot-scope="scope">
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-edit"
+ @click="handleUpdate(scope.row)"
+ v-hasPermi="['${moduleName}:${businessName}:edit']"
+ >修改</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-delete"
+ @click="handleDelete(scope.row)"
+ v-hasPermi="['${moduleName}:${businessName}:remove']"
+ >删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ :page.sync="queryParams.pageNum"
+ :limit.sync="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 添加或修改${functionName}对话框 -->
+ <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+ <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+#foreach($column in $columns)
+#set($field=$column.javaField)
+#if($column.insert && !$column.pk)
+#if(($column.usableColumn) || (!$column.superColumn))
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#set($dictType=$column.dictType)
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" placeholder="请输入${comment}" />
+ </el-form-item>
+#elseif($column.htmlType == "imageUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <image-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "fileUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <file-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "editor")
+ <el-form-item label="${comment}">
+ <editor v-model="form.${field}" :min-height="192"/>
+ </el-form-item>
+#elseif($column.htmlType == "select" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :value="parseInt(dict.value)"
+#else
+ :value="dict.value"
+#end
+ ></el-option>
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "select" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+ :label="dict.value">
+ {{dict.label}}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox>请选择字典生成</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio
+ v-for="dict in dict.type.${dictType}"
+ :key="dict.value"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :label="parseInt(dict.value)"
+#else
+ :label="dict.value"
+#end
+ >{{dict.label}}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio label="1">请选择字典生成</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "datetime")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-date-picker clearable
+ v-model="form.${field}"
+ type="date"
+ value-format="yyyy-MM-dd"
+ placeholder="请选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "textarea")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+#end
+#end
+#end
+#end
+#if($table.sub)
+ <el-divider content-position="center">${subTable.functionName}信息</el-divider>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd${subClassName}">添加</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" icon="el-icon-delete" size="mini" @click="handleDelete${subClassName}">删除</el-button>
+ </el-col>
+ </el-row>
+ <el-table :data="${subclassName}List" :row-class-name="row${subClassName}Index" @selection-change="handle${subClassName}SelectionChange" ref="${subclassName}">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="序号" align="center" prop="index" width="50"/>
+#foreach($column in $subTable.columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk || $javaField == ${subTableFkclassName})
+#elseif($column.list && $column.htmlType == "input")
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template slot-scope="scope">
+ <el-input v-model="scope.row.$javaField" placeholder="请输入$comment" />
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="$comment" prop="${javaField}" width="240">
+ <template slot-scope="scope">
+ <el-date-picker clearable v-model="scope.row.$javaField" type="date" value-format="yyyy-MM-dd" placeholder="请选择$comment" />
+ </template>
+ </el-table-column>
+#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" != $column.dictType)
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template slot-scope="scope">
+ <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
+ <el-option
+ v-for="dict in dict.type.$column.dictType"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ ></el-option>
+ </el-select>
+ </template>
+ </el-table-column>
+#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" == $column.dictType)
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template slot-scope="scope">
+ <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </template>
+ </el-table-column>
+#end
+#end
+ </el-table>
+#end
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
+
+export default {
+ name: "${BusinessName}",
+#if(${dicts} != '')
+ dicts: [${dicts}],
+#end
+ data() {
+ return {
+ // 遮罩层
+ loading: true,
+ // 选中数组
+ ids: [],
+#if($table.sub)
+ // 子表选中数据
+ checked${subClassName}: [],
+#end
+ // 非单个禁用
+ single: true,
+ // 非多个禁用
+ multiple: true,
+ // 显示搜索条件
+ showSearch: true,
+ // 总条数
+ total: 0,
+ // ${functionName}表格数据
+ ${businessName}List: [],
+#if($table.sub)
+ // ${subTable.functionName}表格数据
+ ${subclassName}List: [],
+#end
+ // 弹出层标题
+ title: "",
+ // 是否显示弹出层
+ open: false,
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ // $comment时间范围
+ daterange${AttrName}: [],
+#end
+#end
+ // 查询参数
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+#foreach ($column in $columns)
+#if($column.query)
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ },
+ // 表单参数
+ form: {},
+ // 表单校验
+ rules: {
+#foreach ($column in $columns)
+#if($column.required)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+ $column.javaField: [
+ { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
+ ]#if($foreach.count != $columns.size()),#end
+#end
+#end
+ }
+ };
+ },
+ created() {
+ this.getList();
+ },
+ methods: {
+ /** 查询${functionName}列表 */
+ getList() {
+ this.loading = true;
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ this.queryParams.params = {};
+#break
+#end
+#end
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ if (null != this.daterange${AttrName} && '' != this.daterange${AttrName}) {
+ this.queryParams.params["begin${AttrName}"] = this.daterange${AttrName}[0];
+ this.queryParams.params["end${AttrName}"] = this.daterange${AttrName}[1];
+ }
+#end
+#end
+ list${BusinessName}(this.queryParams).then(response => {
+ this.${businessName}List = response.rows;
+ this.total = response.total;
+ this.loading = false;
+ });
+ },
+ // 取消按钮
+ cancel() {
+ this.open = false;
+ this.reset();
+ },
+ // 表单重置
+ reset() {
+ this.form = {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ $column.javaField: []#if($foreach.count != $columns.size()),#end
+#else
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ };
+#if($table.sub)
+ this.${subclassName}List = [];
+#end
+ this.resetForm("form");
+ },
+ /** 搜索按钮操作 */
+ handleQuery() {
+ this.queryParams.pageNum = 1;
+ this.getList();
+ },
+ /** 重置按钮操作 */
+ resetQuery() {
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ this.daterange${AttrName} = [];
+#end
+#end
+ this.resetForm("queryForm");
+ this.handleQuery();
+ },
+ // 多选框选中数据
+ handleSelectionChange(selection) {
+ this.ids = selection.map(item => item.${pkColumn.javaField})
+ this.single = selection.length!==1
+ this.multiple = !selection.length
+ },
+ /** 新增按钮操作 */
+ handleAdd() {
+ this.reset();
+ this.open = true;
+ this.title = "添加${functionName}";
+ },
+ /** 修改按钮操作 */
+ handleUpdate(row) {
+ this.reset();
+ const ${pkColumn.javaField} = row.${pkColumn.javaField} || this.ids
+ get${BusinessName}(${pkColumn.javaField}).then(response => {
+ this.form = response.data;
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ this.form.$column.javaField = this.form.${column.javaField}.split(",");
+#end
+#end
+#if($table.sub)
+ this.${subclassName}List = response.data.${subclassName}List;
+#end
+ this.open = true;
+ this.title = "修改${functionName}";
+ });
+ },
+ /** 提交按钮 */
+ submitForm() {
+ this.#[[$]]#refs["form"].validate(valid => {
+ if (valid) {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ this.form.$column.javaField = this.form.${column.javaField}.join(",");
+#end
+#end
+#if($table.sub)
+ this.form.${subclassName}List = this.${subclassName}List;
+#end
+ if (this.form.${pkColumn.javaField} != null) {
+ update${BusinessName}(this.form).then(response => {
+ this.#[[$modal]]#.msgSuccess("修改成功");
+ this.open = false;
+ this.getList();
+ });
+ } else {
+ add${BusinessName}(this.form).then(response => {
+ this.#[[$modal]]#.msgSuccess("新增成功");
+ this.open = false;
+ this.getList();
+ });
+ }
+ }
+ });
+ },
+ /** 删除按钮操作 */
+ handleDelete(row) {
+ const ${pkColumn.javaField}s = row.${pkColumn.javaField} || this.ids;
+ this.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + ${pkColumn.javaField}s + '"的数据项?').then(function() {
+ return del${BusinessName}(${pkColumn.javaField}s);
+ }).then(() => {
+ this.getList();
+ this.#[[$modal]]#.msgSuccess("删除成功");
+ }).catch(() => {});
+ },
+#if($table.sub)
+ /** ${subTable.functionName}序号 */
+ row${subClassName}Index({ row, rowIndex }) {
+ row.index = rowIndex + 1;
+ },
+ /** ${subTable.functionName}添加按钮操作 */
+ handleAdd${subClassName}() {
+ let obj = {};
+#foreach($column in $subTable.columns)
+#if($column.pk || $column.javaField == ${subTableFkclassName})
+#elseif($column.list && "" != $javaField)
+ obj.$column.javaField = "";
+#end
+#end
+ this.${subclassName}List.push(obj);
+ },
+ /** ${subTable.functionName}删除按钮操作 */
+ handleDelete${subClassName}() {
+ if (this.checked${subClassName}.length == 0) {
+ this.#[[$modal]]#.msgError("请先选择要删除的${subTable.functionName}数据");
+ } else {
+ const ${subclassName}List = this.${subclassName}List;
+ const checked${subClassName} = this.checked${subClassName};
+ this.${subclassName}List = ${subclassName}List.filter(function(item) {
+ return checked${subClassName}.indexOf(item.index) == -1
+ });
+ }
+ },
+ /** 复选框选中数据 */
+ handle${subClassName}SelectionChange(selection) {
+ this.checked${subClassName} = selection.map(item => item.index)
+ },
+#end
+ /** 导出按钮操作 */
+ handleExport() {
+ this.download('${moduleName}/${businessName}/export', {
+ ...this.queryParams
+ }, `${businessName}_#[[${new Date().getTime()}]]#.xlsx`)
+ }
+ }
+};
+</script>
diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm
new file mode 100644
index 0000000..c54d62b
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm
@@ -0,0 +1,474 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+#foreach($column in $columns)
+#if($column.query)
+#set($dictType=$column.dictType)
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-input
+ v-model="queryParams.${column.javaField}"
+ placeholder="请输入${comment}"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-date-picker clearable
+ v-model="queryParams.${column.javaField}"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ <el-form-item label="${comment}" style="width: 308px">
+ <el-date-picker
+ v-model="daterange${AttrName}"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+#end
+#end
+#end
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['${moduleName}:${businessName}:add']"
+ >新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Sort"
+ @click="toggleExpandAll"
+ >展开/折叠</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table
+ v-if="refreshTable"
+ v-loading="loading"
+ :data="${businessName}List"
+ row-key="${treeCode}"
+ :default-expand-all="isExpandAll"
+ :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+ >
+#foreach($column in $columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk)
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+ <template #default="scope">
+ <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $column.dictType)
+ <el-table-column label="${comment}" align="center" prop="${javaField}">
+ <template #default="scope">
+#if($column.htmlType == "checkbox")
+ <dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
+#else
+ <dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
+#end
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $javaField)
+#if(${foreach.index} == 1)
+ <el-table-column label="${comment}" prop="${javaField}" />
+#else
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#end
+#end
+#end
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 添加或修改${functionName}对话框 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
+#foreach($column in $columns)
+#set($field=$column.javaField)
+#if($column.insert && !$column.pk)
+#if(($column.usableColumn) || (!$column.superColumn))
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#set($dictType=$column.dictType)
+#if("" != $treeParentCode && $column.javaField == $treeParentCode)
+ <el-form-item label="${comment}" prop="${treeParentCode}">
+ <el-tree-select
+ v-model="form.${treeParentCode}"
+ :data="${businessName}Options"
+ :props="{ value: '${treeCode}', label: '${treeName}', children: 'children' }"
+ value-key="${treeCode}"
+ placeholder="请选择${comment}"
+ check-strictly
+ />
+ </el-form-item>
+#elseif($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" placeholder="请输入${comment}" />
+ </el-form-item>
+#elseif($column.htmlType == "imageUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <image-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "fileUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <file-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "editor")
+ <el-form-item label="${comment}">
+ <editor v-model="form.${field}" :min-height="192"/>
+ </el-form-item>
+#elseif($column.htmlType == "select" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :value="parseInt(dict.value)"
+#else
+ :value="dict.value"
+#end
+ ></el-option>
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "select" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.value">
+ {{dict.label}}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox>请选择字典生成</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :label="parseInt(dict.value)"
+#else
+ :label="dict.value"
+#end
+ >{{dict.label}}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio label="1">请选择字典生成</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "datetime")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-date-picker clearable
+ v-model="form.${field}"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "textarea")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+#end
+#end
+#end
+#end
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="${BusinessName}">
+import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
+
+const { proxy } = getCurrentInstance();
+#if(${dicts} != '')
+#set($dictsNoSymbol=$dicts.replace("'", ""))
+const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
+#end
+
+const ${businessName}List = ref([]);
+const ${businessName}Options = ref([]);
+const open = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const title = ref("");
+const isExpandAll = ref(true);
+const refreshTable = ref(true);
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+const daterange${AttrName} = ref([]);
+#end
+#end
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ #foreach ($column in $columns)
+#if($column.query)
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ },
+ rules: {
+ #foreach ($column in $columns)
+#if($column.required)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+ $column.javaField: [
+ { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
+ ]#if($foreach.count != $columns.size()),#end
+#end
+#end
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询${functionName}列表 */
+function getList() {
+ loading.value = true;
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ queryParams.value.params = {};
+#break
+#end
+#end
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ if (null != daterange${AttrName} && '' != daterange${AttrName}) {
+ queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
+ queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
+ }
+#end
+#end
+ list${BusinessName}(queryParams.value).then(response => {
+ ${businessName}List.value = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
+ loading.value = false;
+ });
+}
+
+/** 查询${functionName}下拉树结构 */
+function getTreeselect() {
+ list${BusinessName}().then(response => {
+ ${businessName}Options.value = [];
+ const data = { ${treeCode}: 0, ${treeName}: '顶级节点', children: [] };
+ data.children = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
+ ${businessName}Options.value.push(data);
+ });
+}
+
+// 取消按钮
+function cancel() {
+ open.value = false;
+ reset();
+}
+
+// 表单重置
+function reset() {
+ form.value = {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ $column.javaField: []#if($foreach.count != $columns.size()),#end
+#else
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ };
+ proxy.resetForm("${businessName}Ref");
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+ getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ daterange${AttrName}.value = [];
+#end
+#end
+ proxy.resetForm("queryRef");
+ handleQuery();
+}
+
+/** 新增按钮操作 */
+function handleAdd(row) {
+ reset();
+ getTreeselect();
+ if (row != null && row.${treeCode}) {
+ form.value.${treeParentCode} = row.${treeCode};
+ } else {
+ form.value.${treeParentCode} = 0;
+ }
+ open.value = true;
+ title.value = "添加${functionName}";
+}
+
+/** 展开/折叠操作 */
+function toggleExpandAll() {
+ refreshTable.value = false;
+ isExpandAll.value = !isExpandAll.value;
+ nextTick(() => {
+ refreshTable.value = true;
+ });
+}
+
+/** 修改按钮操作 */
+async function handleUpdate(row) {
+ reset();
+ await getTreeselect();
+ if (row != null) {
+ form.value.${treeParentCode} = row.${treeParentCode};
+ }
+ get${BusinessName}(row.${pkColumn.javaField}).then(response => {
+ form.value = response.data;
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ form.value.$column.javaField = form.value.${column.javaField}.split(",");
+#end
+#end
+ open.value = true;
+ title.value = "修改${functionName}";
+ });
+}
+
+/** 提交按钮 */
+function submitForm() {
+ proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
+ if (valid) {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ form.value.$column.javaField = form.value.${column.javaField}.join(",");
+#end
+#end
+ if (form.value.${pkColumn.javaField} != null) {
+ update${BusinessName}(form.value).then(response => {
+ proxy.#[[$modal]]#.msgSuccess("修改成功");
+ open.value = false;
+ getList();
+ });
+ } else {
+ add${BusinessName}(form.value).then(response => {
+ proxy.#[[$modal]]#.msgSuccess("新增成功");
+ open.value = false;
+ getList();
+ });
+ }
+ }
+ });
+}
+
+/** 删除按钮操作 */
+function handleDelete(row) {
+ proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?').then(function() {
+ return del${BusinessName}(row.${pkColumn.javaField});
+ }).then(() => {
+ getList();
+ proxy.#[[$modal]]#.msgSuccess("删除成功");
+ }).catch(() => {});
+}
+
+getList();
+</script>
diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm
new file mode 100644
index 0000000..8b25665
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm
@@ -0,0 +1,590 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+#foreach($column in $columns)
+#if($column.query)
+#set($dictType=$column.dictType)
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-input
+ v-model="queryParams.${column.javaField}"
+ placeholder="请输入${comment}"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+ <el-form-item label="${comment}" prop="${column.javaField}">
+ <el-date-picker clearable
+ v-model="queryParams.${column.javaField}"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="请选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ <el-form-item label="${comment}" style="width: 308px">
+ <el-date-picker
+ v-model="daterange${AttrName}"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+#end
+#end
+#end
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['${moduleName}:${businessName}:add']"
+ >新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['${moduleName}:${businessName}:edit']"
+ >修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['${moduleName}:${businessName}:remove']"
+ >删除</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['${moduleName}:${businessName}:export']"
+ >导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="${businessName}List" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+#foreach($column in $columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk)
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+ <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+ <template #default="scope">
+ <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $column.dictType)
+ <el-table-column label="${comment}" align="center" prop="${javaField}">
+ <template #default="scope">
+#if($column.htmlType == "checkbox")
+ <dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
+#else
+ <dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
+#end
+ </template>
+ </el-table-column>
+#elseif($column.list && "" != $javaField)
+ <el-table-column label="${comment}" align="center" prop="${javaField}" />
+#end
+#end
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 添加或修改${functionName}对话框 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
+#foreach($column in $columns)
+#set($field=$column.javaField)
+#if($column.insert && !$column.pk)
+#if(($column.usableColumn) || (!$column.superColumn))
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#set($dictType=$column.dictType)
+#if($column.htmlType == "input")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" placeholder="请输入${comment}" />
+ </el-form-item>
+#elseif($column.htmlType == "imageUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <image-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "fileUpload")
+ <el-form-item label="${comment}" prop="${field}">
+ <file-upload v-model="form.${field}"/>
+ </el-form-item>
+#elseif($column.htmlType == "editor")
+ <el-form-item label="${comment}">
+ <editor v-model="form.${field}" :min-height="192"/>
+ </el-form-item>
+#elseif($column.htmlType == "select" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.label"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :value="parseInt(dict.value)"
+#else
+ :value="dict.value"
+#end
+ ></el-option>
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "select" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-select v-model="form.${field}" placeholder="请选择${comment}">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+ :label="dict.value">
+ {{dict.label}}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "checkbox" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-checkbox-group v-model="form.${field}">
+ <el-checkbox>请选择字典生成</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && "" != $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio
+ v-for="dict in ${dictType}"
+ :key="dict.value"
+#if($column.javaType == "Integer" || $column.javaType == "Long")
+ :label="parseInt(dict.value)"
+#else
+ :label="dict.value"
+#end
+ >{{dict.label}}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "radio" && $dictType)
+ <el-form-item label="${comment}" prop="${field}">
+ <el-radio-group v-model="form.${field}">
+ <el-radio label="1">请选择字典生成</el-radio>
+ </el-radio-group>
+ </el-form-item>
+#elseif($column.htmlType == "datetime")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-date-picker clearable
+ v-model="form.${field}"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="请选择${comment}">
+ </el-date-picker>
+ </el-form-item>
+#elseif($column.htmlType == "textarea")
+ <el-form-item label="${comment}" prop="${field}">
+ <el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+#end
+#end
+#end
+#end
+#if($table.sub)
+ <el-divider content-position="center">${subTable.functionName}信息</el-divider>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" icon="Plus" @click="handleAdd${subClassName}">添加</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" icon="Delete" @click="handleDelete${subClassName}">删除</el-button>
+ </el-col>
+ </el-row>
+ <el-table :data="${subclassName}List" :row-class-name="row${subClassName}Index" @selection-change="handle${subClassName}SelectionChange" ref="${subclassName}">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="序号" align="center" prop="index" width="50"/>
+#foreach($column in $subTable.columns)
+#set($javaField=$column.javaField)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+#if($column.pk || $javaField == ${subTableFkclassName})
+#elseif($column.list && $column.htmlType == "input")
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template #default="scope">
+ <el-input v-model="scope.row.$javaField" placeholder="请输入$comment" />
+ </template>
+ </el-table-column>
+#elseif($column.list && $column.htmlType == "datetime")
+ <el-table-column label="$comment" prop="${javaField}" width="240">
+ <template #default="scope">
+ <el-date-picker clearable
+ v-model="scope.row.$javaField"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="请选择$comment">
+ </el-date-picker>
+ </template>
+ </el-table-column>
+#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" != $column.dictType)
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template #default="scope">
+ <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
+ <el-option
+ v-for="dict in $column.dictType"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ ></el-option>
+ </el-select>
+ </template>
+ </el-table-column>
+#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" == $column.dictType)
+ <el-table-column label="$comment" prop="${javaField}" width="150">
+ <template #default="scope">
+ <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
+ <el-option label="请选择字典生成" value="" />
+ </el-select>
+ </template>
+ </el-table-column>
+#end
+#end
+ </el-table>
+#end
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="${BusinessName}">
+import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
+
+const { proxy } = getCurrentInstance();
+#if(${dicts} != '')
+#set($dictsNoSymbol=$dicts.replace("'", ""))
+const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
+#end
+
+const ${businessName}List = ref([]);
+#if($table.sub)
+const ${subclassName}List = ref([]);
+#end
+const open = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref([]);
+#if($table.sub)
+const checked${subClassName} = ref([]);
+#end
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref("");
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+const daterange${AttrName} = ref([]);
+#end
+#end
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ #foreach ($column in $columns)
+#if($column.query)
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ },
+ rules: {
+ #foreach ($column in $columns)
+#if($column.required)
+#set($parentheseIndex=$column.columnComment.indexOf("("))
+#if($parentheseIndex != -1)
+#set($comment=$column.columnComment.substring(0, $parentheseIndex))
+#else
+#set($comment=$column.columnComment)
+#end
+ $column.javaField: [
+ { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
+ ]#if($foreach.count != $columns.size()),#end
+#end
+#end
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询${functionName}列表 */
+function getList() {
+ loading.value = true;
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+ queryParams.value.params = {};
+#break
+#end
+#end
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ if (null != daterange${AttrName} && '' != daterange${AttrName}) {
+ queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
+ queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
+ }
+#end
+#end
+ list${BusinessName}(queryParams.value).then(response => {
+ ${businessName}List.value = response.rows;
+ total.value = response.total;
+ loading.value = false;
+ });
+}
+
+// 取消按钮
+function cancel() {
+ open.value = false;
+ reset();
+}
+
+// 表单重置
+function reset() {
+ form.value = {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ $column.javaField: []#if($foreach.count != $columns.size()),#end
+#else
+ $column.javaField: null#if($foreach.count != $columns.size()),#end
+#end
+#end
+ };
+#if($table.sub)
+ ${subclassName}List.value = [];
+#end
+ proxy.resetForm("${businessName}Ref");
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+#foreach ($column in $columns)
+#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+ daterange${AttrName}.value = [];
+#end
+#end
+ proxy.resetForm("queryRef");
+ handleQuery();
+}
+
+// 多选框选中数据
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.${pkColumn.javaField});
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+function handleAdd() {
+ reset();
+ open.value = true;
+ title.value = "添加${functionName}";
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row) {
+ reset();
+ const _${pkColumn.javaField} = row.${pkColumn.javaField} || ids.value
+ get${BusinessName}(_${pkColumn.javaField}).then(response => {
+ form.value = response.data;
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ form.value.$column.javaField = form.value.${column.javaField}.split(",");
+#end
+#end
+#if($table.sub)
+ ${subclassName}List.value = response.data.${subclassName}List;
+#end
+ open.value = true;
+ title.value = "修改${functionName}";
+ });
+}
+
+/** 提交按钮 */
+function submitForm() {
+ proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
+ if (valid) {
+#foreach ($column in $columns)
+#if($column.htmlType == "checkbox")
+ form.value.$column.javaField = form.value.${column.javaField}.join(",");
+#end
+#end
+#if($table.sub)
+ form.value.${subclassName}List = ${subclassName}List.value;
+#end
+ if (form.value.${pkColumn.javaField} != null) {
+ update${BusinessName}(form.value).then(response => {
+ proxy.#[[$modal]]#.msgSuccess("修改成功");
+ open.value = false;
+ getList();
+ });
+ } else {
+ add${BusinessName}(form.value).then(response => {
+ proxy.#[[$modal]]#.msgSuccess("新增成功");
+ open.value = false;
+ getList();
+ });
+ }
+ }
+ });
+}
+
+/** 删除按钮操作 */
+function handleDelete(row) {
+ const _${pkColumn.javaField}s = row.${pkColumn.javaField} || ids.value;
+ proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?').then(function() {
+ return del${BusinessName}(_${pkColumn.javaField}s);
+ }).then(() => {
+ getList();
+ proxy.#[[$modal]]#.msgSuccess("删除成功");
+ }).catch(() => {});
+}
+
+#if($table.sub)
+/** ${subTable.functionName}序号 */
+function row${subClassName}Index({ row, rowIndex }) {
+ row.index = rowIndex + 1;
+}
+
+/** ${subTable.functionName}添加按钮操作 */
+function handleAdd${subClassName}() {
+ let obj = {};
+#foreach($column in $subTable.columns)
+#if($column.pk || $column.javaField == ${subTableFkclassName})
+#elseif($column.list && "" != $javaField)
+ obj.$column.javaField = "";
+#end
+#end
+ ${subclassName}List.value.push(obj);
+}
+
+/** ${subTable.functionName}删除按钮操作 */
+function handleDelete${subClassName}() {
+ if (checked${subClassName}.value.length == 0) {
+ proxy.#[[$modal]]#.msgError("请先选择要删除的${subTable.functionName}数据");
+ } else {
+ const ${subclassName}s = ${subclassName}List.value;
+ const checked${subClassName}s = checked${subClassName}.value;
+ ${subclassName}List.value = ${subclassName}s.filter(function(item) {
+ return checked${subClassName}s.indexOf(item.index) == -1
+ });
+ }
+}
+
+/** 复选框选中数据 */
+function handle${subClassName}SelectionChange(selection) {
+ checked${subClassName}.value = selection.map(item => item.index)
+}
+
+#end
+/** 导出按钮操作 */
+function handleExport() {
+ proxy.download('${moduleName}/${businessName}/export', {
+ ...queryParams.value
+ }, `${businessName}_#[[${new Date().getTime()}]]#.xlsx`)
+}
+
+getList();
+</script>
diff --git a/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm
new file mode 100644
index 0000000..456755b
--- /dev/null
+++ b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
+
+ <resultMap type="${ClassName}" id="${ClassName}Result">
+#foreach ($column in $columns)
+ <result property="${column.javaField}" column="${column.columnName}" />
+#end
+ </resultMap>
+#if($table.sub)
+
+ <resultMap id="${ClassName}${subClassName}Result" type="${ClassName}" extends="${ClassName}Result">
+ <collection property="${subclassName}List" ofType="${subClassName}" column="${pkColumn.columnName}" select="select${subClassName}List" />
+ </resultMap>
+
+ <resultMap type="${subClassName}" id="${subClassName}Result">
+#foreach ($column in $subTable.columns)
+ <result property="${column.javaField}" column="${column.columnName}" />
+#end
+ </resultMap>
+#end
+
+ <sql id="select${ClassName}Vo">
+ select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName}
+ </sql>
+
+ <select id="select${ClassName}List" parameterType="${ClassName}" resultMap="${ClassName}Result">
+ <include refid="select${ClassName}Vo"/>
+ <where>
+#foreach($column in $columns)
+#set($queryType=$column.queryType)
+#set($javaField=$column.javaField)
+#set($javaType=$column.javaType)
+#set($columnName=$column.columnName)
+#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+#if($column.query)
+#if($column.queryType == "EQ")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName = #{$javaField}</if>
+#elseif($queryType == "NE")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName != #{$javaField}</if>
+#elseif($queryType == "GT")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName > #{$javaField}</if>
+#elseif($queryType == "GTE")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName >= #{$javaField}</if>
+#elseif($queryType == "LT")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName < #{$javaField}</if>
+#elseif($queryType == "LTE")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName <= #{$javaField}</if>
+#elseif($queryType == "LIKE")
+ <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName like concat('%', #{$javaField}, '%')</if>
+#elseif($queryType == "BETWEEN")
+ <if test="params.begin$AttrName != null and params.begin$AttrName != '' and params.end$AttrName != null and params.end$AttrName != ''"> and $columnName between #{params.begin$AttrName} and #{params.end$AttrName}</if>
+#end
+#end
+#end
+ </where>
+ </select>
+
+ <select id="select${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}" resultMap="#if($table.sub)${ClassName}${subClassName}Result#else${ClassName}Result#end">
+#if($table.crud || $table.tree)
+ <include refid="select${ClassName}Vo"/>
+ where ${pkColumn.columnName} = #{${pkColumn.javaField}}
+#elseif($table.sub)
+ select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end
+ from ${tableName}
+ where ${pkColumn.columnName} = #{${pkColumn.javaField}}
+#end
+ </select>
+#if($table.sub)
+
+ <select id="select${subClassName}List" resultMap="${subClassName}Result">
+ select#foreach ($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end
+ from ${subTableName}
+ where ${subTableFkName} = #{${subTableFkName}}
+ </select>
+#end
+
+ <insert id="insert${ClassName}" parameterType="${ClassName}"#if($pkColumn.increment) useGeneratedKeys="true" keyProperty="$pkColumn.javaField"#end>
+ insert into ${tableName}
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+#foreach($column in $columns)
+#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
+ <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">$column.columnName,</if>
+#end
+#end
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+#foreach($column in $columns)
+#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
+ <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">#{$column.javaField},</if>
+#end
+#end
+ </trim>
+ </insert>
+
+ <update id="update${ClassName}" parameterType="${ClassName}">
+ update ${tableName}
+ <trim prefix="SET" suffixOverrides=",">
+#foreach($column in $columns)
+#if($column.columnName != $pkColumn.columnName)
+ <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">$column.columnName = #{$column.javaField},</if>
+#end
+#end
+ </trim>
+ where ${pkColumn.columnName} = #{${pkColumn.javaField}}
+ </update>
+
+ <delete id="delete${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}">
+ delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
+ </delete>
+
+ <delete id="delete${ClassName}By${pkColumn.capJavaField}s" parameterType="String">
+ delete from ${tableName} where ${pkColumn.columnName} in
+ <foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
+ #{${pkColumn.javaField}}
+ </foreach>
+ </delete>
+#if($table.sub)
+
+ <delete id="delete${subClassName}By${subTableFkClassName}s" parameterType="String">
+ delete from ${subTableName} where ${subTableFkName} in
+ <foreach item="${subTableFkclassName}" collection="array" open="(" separator="," close=")">
+ #{${subTableFkclassName}}
+ </foreach>
+ </delete>
+
+ <delete id="delete${subClassName}By${subTableFkClassName}" parameterType="${pkColumn.javaType}">
+ delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}}
+ </delete>
+
+ <insert id="batch${subClassName}">
+ insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values
+ <foreach item="item" index="index" collection="list" separator=",">
+ (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end)
+ </foreach>
+ </insert>
+#end
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-quartz/pom.xml b/ruoyi-quartz/pom.xml
new file mode 100644
index 0000000..a6d75e4
--- /dev/null
+++ b/ruoyi-quartz/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-quartz</artifactId>
+
+ <description>
+ quartz定时任务
+ </description>
+
+ <dependencies>
+
+ <!-- 定时任务 -->
+ <dependency>
+ <groupId>org.quartz-scheduler</groupId>
+ <artifactId>quartz</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>com.mchange</groupId>
+ <artifactId>c3p0</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- 通用工具-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>6.0.0</version>
+ <scope>compile</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java
new file mode 100644
index 0000000..d4e065a
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java
@@ -0,0 +1,57 @@
+//package com.ruoyi.quartz.config;
+//
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.scheduling.quartz.SchedulerFactoryBean;
+//import javax.sql.DataSource;
+//import java.util.Properties;
+//
+///**
+// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效)
+// *
+// * @author ruoyi
+// */
+//@Configuration
+//public class ScheduleConfig
+//{
+// @Bean
+// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource)
+// {
+// SchedulerFactoryBean factory = new SchedulerFactoryBean();
+// factory.setDataSource(dataSource);
+//
+// // quartz参数
+// Properties prop = new Properties();
+// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler");
+// prop.put("org.quartz.scheduler.instanceId", "AUTO");
+// // 线程池配置
+// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
+// prop.put("org.quartz.threadPool.threadCount", "20");
+// prop.put("org.quartz.threadPool.threadPriority", "5");
+// // JobStore配置
+// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
+// // 集群配置
+// prop.put("org.quartz.jobStore.isClustered", "true");
+// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
+// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10");
+// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
+//
+// // sqlserver 启用
+// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
+// prop.put("org.quartz.jobStore.misfireThreshold", "12000");
+// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
+// factory.setQuartzProperties(prop);
+//
+// factory.setSchedulerName("RuoyiScheduler");
+// // 延时启动
+// factory.setStartupDelay(1);
+// factory.setApplicationContextSchedulerContextKey("applicationContextKey");
+// // 可选,QuartzScheduler
+// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
+// factory.setOverwriteExistingJobs(true);
+// // 设置自动启动,默认为true
+// factory.setAutoStartup(true);
+//
+// return factory;
+// }
+//}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java
new file mode 100644
index 0000000..e826dfd
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java
@@ -0,0 +1,185 @@
+package com.ruoyi.quartz.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.quartz.SchedulerException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.exception.job.TaskException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.quartz.domain.SysJob;
+import com.ruoyi.quartz.service.ISysJobService;
+import com.ruoyi.quartz.util.CronUtils;
+import com.ruoyi.quartz.util.ScheduleUtils;
+
+/**
+ * 调度任务信息操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/job")
+public class SysJobController extends BaseController
+{
+ @Autowired
+ private ISysJobService jobService;
+
+ /**
+ * 查询定时任务列表
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysJob sysJob)
+ {
+ startPage();
+ List<SysJob> list = jobService.selectJobList(sysJob);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出定时任务列表
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:export')")
+ @Log(title = "定时任务", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysJob sysJob)
+ {
+ List<SysJob> list = jobService.selectJobList(sysJob);
+ ExcelUtil<SysJob> util = new ExcelUtil<SysJob>(SysJob.class);
+ util.exportExcel(response, list, "定时任务");
+ }
+
+ /**
+ * 获取定时任务详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:query')")
+ @GetMapping(value = "/{jobId}")
+ public AjaxResult getInfo(@PathVariable("jobId") Long jobId)
+ {
+ return success(jobService.selectJobById(jobId));
+ }
+
+ /**
+ * 新增定时任务
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:add')")
+ @Log(title = "定时任务", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
+ {
+ if (!CronUtils.isValid(job.getCronExpression()))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+ }
+ else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+ }
+ else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
+ {
+ return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+ }
+ job.setCreateBy(getUsername());
+ return toAjax(jobService.insertJob(job));
+ }
+
+ /**
+ * 修改定时任务
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
+ @Log(title = "定时任务", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
+ {
+ if (!CronUtils.isValid(job.getCronExpression()))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+ }
+ else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+ }
+ else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+ }
+ else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
+ {
+ return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+ }
+ job.setUpdateBy(getUsername());
+ return toAjax(jobService.updateJob(job));
+ }
+
+ /**
+ * 定时任务状态修改
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
+ @Log(title = "定时任务", businessType = BusinessType.UPDATE)
+ @PutMapping("/changeStatus")
+ public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
+ {
+ SysJob newJob = jobService.selectJobById(job.getJobId());
+ newJob.setStatus(job.getStatus());
+ return toAjax(jobService.changeStatus(newJob));
+ }
+
+ /**
+ * 定时任务立即执行一次
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
+ @Log(title = "定时任务", businessType = BusinessType.UPDATE)
+ @PutMapping("/run")
+ public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
+ {
+ boolean result = jobService.run(job);
+ return result ? success() : error("任务不存在或已过期!");
+ }
+
+ /**
+ * 删除定时任务
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
+ @Log(title = "定时任务", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{jobIds}")
+ public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException
+ {
+ jobService.deleteJobByIds(jobIds);
+ return success();
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java
new file mode 100644
index 0000000..233361b
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java
@@ -0,0 +1,92 @@
+package com.ruoyi.quartz.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.quartz.domain.SysJobLog;
+import com.ruoyi.quartz.service.ISysJobLogService;
+
+/**
+ * 调度日志操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/jobLog")
+public class SysJobLogController extends BaseController
+{
+ @Autowired
+ private ISysJobLogService jobLogService;
+
+ /**
+ * 查询定时任务调度日志列表
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysJobLog sysJobLog)
+ {
+ startPage();
+ List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出定时任务调度日志列表
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:export')")
+ @Log(title = "任务调度日志", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysJobLog sysJobLog)
+ {
+ List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+ ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
+ util.exportExcel(response, list, "调度日志");
+ }
+
+ /**
+ * 根据调度编号获取详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:query')")
+ @GetMapping(value = "/{jobLogId}")
+ public AjaxResult getInfo(@PathVariable Long jobLogId)
+ {
+ return success(jobLogService.selectJobLogById(jobLogId));
+ }
+
+
+ /**
+ * 删除定时任务调度日志
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
+ @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{jobLogIds}")
+ public AjaxResult remove(@PathVariable Long[] jobLogIds)
+ {
+ return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
+ }
+
+ /**
+ * 清空定时任务调度日志
+ */
+ @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
+ @Log(title = "调度日志", businessType = BusinessType.CLEAN)
+ @DeleteMapping("/clean")
+ public AjaxResult clean()
+ {
+ jobLogService.cleanJobLog();
+ return success();
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java
new file mode 100644
index 0000000..cea12dc
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java
@@ -0,0 +1,171 @@
+package com.ruoyi.quartz.domain;
+
+import java.util.Date;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.constant.ScheduleConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.quartz.util.CronUtils;
+
+/**
+ * 定时任务调度表 sys_job
+ *
+ * @author ruoyi
+ */
+public class SysJob extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 任务ID */
+ @Excel(name = "任务序号", cellType = ColumnType.NUMERIC)
+ private Long jobId;
+
+ /** 任务名称 */
+ @Excel(name = "任务名称")
+ private String jobName;
+
+ /** 任务组名 */
+ @Excel(name = "任务组名")
+ private String jobGroup;
+
+ /** 调用目标字符串 */
+ @Excel(name = "调用目标字符串")
+ private String invokeTarget;
+
+ /** cron执行表达式 */
+ @Excel(name = "执行表达式 ")
+ private String cronExpression;
+
+ /** cron计划策略 */
+ @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行")
+ private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT;
+
+ /** 是否并发执行(0允许 1禁止) */
+ @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止")
+ private String concurrent;
+
+ /** 任务状态(0正常 1暂停) */
+ @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停")
+ private String status;
+
+ public Long getJobId()
+ {
+ return jobId;
+ }
+
+ public void setJobId(Long jobId)
+ {
+ this.jobId = jobId;
+ }
+
+ @NotBlank(message = "任务名称不能为空")
+ @Size(min = 0, max = 64, message = "任务名称不能超过64个字符")
+ public String getJobName()
+ {
+ return jobName;
+ }
+
+ public void setJobName(String jobName)
+ {
+ this.jobName = jobName;
+ }
+
+ public String getJobGroup()
+ {
+ return jobGroup;
+ }
+
+ public void setJobGroup(String jobGroup)
+ {
+ this.jobGroup = jobGroup;
+ }
+
+ @NotBlank(message = "调用目标字符串不能为空")
+ @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符")
+ public String getInvokeTarget()
+ {
+ return invokeTarget;
+ }
+
+ public void setInvokeTarget(String invokeTarget)
+ {
+ this.invokeTarget = invokeTarget;
+ }
+
+ @NotBlank(message = "Cron执行表达式不能为空")
+ @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符")
+ public String getCronExpression()
+ {
+ return cronExpression;
+ }
+
+ public void setCronExpression(String cronExpression)
+ {
+ this.cronExpression = cronExpression;
+ }
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ public Date getNextValidTime()
+ {
+ if (StringUtils.isNotEmpty(cronExpression))
+ {
+ return CronUtils.getNextExecution(cronExpression);
+ }
+ return null;
+ }
+
+ public String getMisfirePolicy()
+ {
+ return misfirePolicy;
+ }
+
+ public void setMisfirePolicy(String misfirePolicy)
+ {
+ this.misfirePolicy = misfirePolicy;
+ }
+
+ public String getConcurrent()
+ {
+ return concurrent;
+ }
+
+ public void setConcurrent(String concurrent)
+ {
+ this.concurrent = concurrent;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("jobId", getJobId())
+ .append("jobName", getJobName())
+ .append("jobGroup", getJobGroup())
+ .append("cronExpression", getCronExpression())
+ .append("nextValidTime", getNextValidTime())
+ .append("misfirePolicy", getMisfirePolicy())
+ .append("concurrent", getConcurrent())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java
new file mode 100644
index 0000000..121c035
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java
@@ -0,0 +1,155 @@
+package com.ruoyi.quartz.domain;
+
+import java.util.Date;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 定时任务调度日志表 sys_job_log
+ *
+ * @author ruoyi
+ */
+public class SysJobLog extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** ID */
+ @Excel(name = "日志序号")
+ private Long jobLogId;
+
+ /** 任务名称 */
+ @Excel(name = "任务名称")
+ private String jobName;
+
+ /** 任务组名 */
+ @Excel(name = "任务组名")
+ private String jobGroup;
+
+ /** 调用目标字符串 */
+ @Excel(name = "调用目标字符串")
+ private String invokeTarget;
+
+ /** 日志信息 */
+ @Excel(name = "日志信息")
+ private String jobMessage;
+
+ /** 执行状态(0正常 1失败) */
+ @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败")
+ private String status;
+
+ /** 异常信息 */
+ @Excel(name = "异常信息")
+ private String exceptionInfo;
+
+ /** 开始时间 */
+ private Date startTime;
+
+ /** 停止时间 */
+ private Date stopTime;
+
+ public Long getJobLogId()
+ {
+ return jobLogId;
+ }
+
+ public void setJobLogId(Long jobLogId)
+ {
+ this.jobLogId = jobLogId;
+ }
+
+ public String getJobName()
+ {
+ return jobName;
+ }
+
+ public void setJobName(String jobName)
+ {
+ this.jobName = jobName;
+ }
+
+ public String getJobGroup()
+ {
+ return jobGroup;
+ }
+
+ public void setJobGroup(String jobGroup)
+ {
+ this.jobGroup = jobGroup;
+ }
+
+ public String getInvokeTarget()
+ {
+ return invokeTarget;
+ }
+
+ public void setInvokeTarget(String invokeTarget)
+ {
+ this.invokeTarget = invokeTarget;
+ }
+
+ public String getJobMessage()
+ {
+ return jobMessage;
+ }
+
+ public void setJobMessage(String jobMessage)
+ {
+ this.jobMessage = jobMessage;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getExceptionInfo()
+ {
+ return exceptionInfo;
+ }
+
+ public void setExceptionInfo(String exceptionInfo)
+ {
+ this.exceptionInfo = exceptionInfo;
+ }
+
+ public Date getStartTime()
+ {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime)
+ {
+ this.startTime = startTime;
+ }
+
+ public Date getStopTime()
+ {
+ return stopTime;
+ }
+
+ public void setStopTime(Date stopTime)
+ {
+ this.stopTime = stopTime;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("jobLogId", getJobLogId())
+ .append("jobName", getJobName())
+ .append("jobGroup", getJobGroup())
+ .append("jobMessage", getJobMessage())
+ .append("status", getStatus())
+ .append("exceptionInfo", getExceptionInfo())
+ .append("startTime", getStartTime())
+ .append("stopTime", getStopTime())
+ .toString();
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java
new file mode 100644
index 0000000..727d916
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java
@@ -0,0 +1,64 @@
+package com.ruoyi.quartz.mapper;
+
+import java.util.List;
+import com.ruoyi.quartz.domain.SysJobLog;
+
+/**
+ * 调度任务日志信息 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysJobLogMapper
+{
+ /**
+ * 获取quartz调度器日志的计划任务
+ *
+ * @param jobLog 调度日志信息
+ * @return 调度任务日志集合
+ */
+ public List<SysJobLog> selectJobLogList(SysJobLog jobLog);
+
+ /**
+ * 查询所有调度任务日志
+ *
+ * @return 调度任务日志列表
+ */
+ public List<SysJobLog> selectJobLogAll();
+
+ /**
+ * 通过调度任务日志ID查询调度信息
+ *
+ * @param jobLogId 调度任务日志ID
+ * @return 调度任务日志对象信息
+ */
+ public SysJobLog selectJobLogById(Long jobLogId);
+
+ /**
+ * 新增任务日志
+ *
+ * @param jobLog 调度日志信息
+ * @return 结果
+ */
+ public int insertJobLog(SysJobLog jobLog);
+
+ /**
+ * 批量删除调度日志信息
+ *
+ * @param logIds 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteJobLogByIds(Long[] logIds);
+
+ /**
+ * 删除任务日志
+ *
+ * @param jobId 调度日志ID
+ * @return 结果
+ */
+ public int deleteJobLogById(Long jobId);
+
+ /**
+ * 清空任务日志
+ */
+ public void cleanJobLog();
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java
new file mode 100644
index 0000000..20f45db
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java
@@ -0,0 +1,67 @@
+package com.ruoyi.quartz.mapper;
+
+import java.util.List;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 调度任务信息 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysJobMapper
+{
+ /**
+ * 查询调度任务日志集合
+ *
+ * @param job 调度信息
+ * @return 操作日志集合
+ */
+ public List<SysJob> selectJobList(SysJob job);
+
+ /**
+ * 查询所有调度任务
+ *
+ * @return 调度任务列表
+ */
+ public List<SysJob> selectJobAll();
+
+ /**
+ * 通过调度ID查询调度任务信息
+ *
+ * @param jobId 调度ID
+ * @return 角色对象信息
+ */
+ public SysJob selectJobById(Long jobId);
+
+ /**
+ * 通过调度ID删除调度任务信息
+ *
+ * @param jobId 调度ID
+ * @return 结果
+ */
+ public int deleteJobById(Long jobId);
+
+ /**
+ * 批量删除调度任务信息
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteJobByIds(Long[] ids);
+
+ /**
+ * 修改调度任务信息
+ *
+ * @param job 调度任务信息
+ * @return 结果
+ */
+ public int updateJob(SysJob job);
+
+ /**
+ * 新增调度任务信息
+ *
+ * @param job 调度任务信息
+ * @return 结果
+ */
+ public int insertJob(SysJob job);
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java
new file mode 100644
index 0000000..8546792
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java
@@ -0,0 +1,56 @@
+package com.ruoyi.quartz.service;
+
+import java.util.List;
+import com.ruoyi.quartz.domain.SysJobLog;
+
+/**
+ * 定时任务调度日志信息信息 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysJobLogService
+{
+ /**
+ * 获取quartz调度器日志的计划任务
+ *
+ * @param jobLog 调度日志信息
+ * @return 调度任务日志集合
+ */
+ public List<SysJobLog> selectJobLogList(SysJobLog jobLog);
+
+ /**
+ * 通过调度任务日志ID查询调度信息
+ *
+ * @param jobLogId 调度任务日志ID
+ * @return 调度任务日志对象信息
+ */
+ public SysJobLog selectJobLogById(Long jobLogId);
+
+ /**
+ * 新增任务日志
+ *
+ * @param jobLog 调度日志信息
+ */
+ public void addJobLog(SysJobLog jobLog);
+
+ /**
+ * 批量删除调度日志信息
+ *
+ * @param logIds 需要删除的日志ID
+ * @return 结果
+ */
+ public int deleteJobLogByIds(Long[] logIds);
+
+ /**
+ * 删除任务日志
+ *
+ * @param jobId 调度日志ID
+ * @return 结果
+ */
+ public int deleteJobLogById(Long jobId);
+
+ /**
+ * 清空任务日志
+ */
+ public void cleanJobLog();
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java
new file mode 100644
index 0000000..437ade8
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java
@@ -0,0 +1,102 @@
+package com.ruoyi.quartz.service;
+
+import java.util.List;
+import org.quartz.SchedulerException;
+import com.ruoyi.common.exception.job.TaskException;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 定时任务调度信息信息 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysJobService
+{
+ /**
+ * 获取quartz调度器的计划任务
+ *
+ * @param job 调度信息
+ * @return 调度任务集合
+ */
+ public List<SysJob> selectJobList(SysJob job);
+
+ /**
+ * 通过调度任务ID查询调度信息
+ *
+ * @param jobId 调度任务ID
+ * @return 调度任务对象信息
+ */
+ public SysJob selectJobById(Long jobId);
+
+ /**
+ * 暂停任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int pauseJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 恢复任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int resumeJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 删除任务后,所对应的trigger也将被删除
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int deleteJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 批量删除调度信息
+ *
+ * @param jobIds 需要删除的任务ID
+ * @return 结果
+ */
+ public void deleteJobByIds(Long[] jobIds) throws SchedulerException;
+
+ /**
+ * 任务调度状态修改
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int changeStatus(SysJob job) throws SchedulerException;
+
+ /**
+ * 立即运行任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public boolean run(SysJob job) throws SchedulerException;
+
+ /**
+ * 新增任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int insertJob(SysJob job) throws SchedulerException, TaskException;
+
+ /**
+ * 更新任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ public int updateJob(SysJob job) throws SchedulerException, TaskException;
+
+ /**
+ * 校验cron表达式是否有效
+ *
+ * @param cronExpression 表达式
+ * @return 结果
+ */
+ public boolean checkCronExpressionIsValid(String cronExpression);
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java
new file mode 100644
index 0000000..812eed7
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java
@@ -0,0 +1,87 @@
+package com.ruoyi.quartz.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.quartz.domain.SysJobLog;
+import com.ruoyi.quartz.mapper.SysJobLogMapper;
+import com.ruoyi.quartz.service.ISysJobLogService;
+
+/**
+ * 定时任务调度日志信息 服务层
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysJobLogServiceImpl implements ISysJobLogService
+{
+ @Autowired
+ private SysJobLogMapper jobLogMapper;
+
+ /**
+ * 获取quartz调度器日志的计划任务
+ *
+ * @param jobLog 调度日志信息
+ * @return 调度任务日志集合
+ */
+ @Override
+ public List<SysJobLog> selectJobLogList(SysJobLog jobLog)
+ {
+ return jobLogMapper.selectJobLogList(jobLog);
+ }
+
+ /**
+ * 通过调度任务日志ID查询调度信息
+ *
+ * @param jobLogId 调度任务日志ID
+ * @return 调度任务日志对象信息
+ */
+ @Override
+ public SysJobLog selectJobLogById(Long jobLogId)
+ {
+ return jobLogMapper.selectJobLogById(jobLogId);
+ }
+
+ /**
+ * 新增任务日志
+ *
+ * @param jobLog 调度日志信息
+ */
+ @Override
+ public void addJobLog(SysJobLog jobLog)
+ {
+ jobLogMapper.insertJobLog(jobLog);
+ }
+
+ /**
+ * 批量删除调度日志信息
+ *
+ * @param logIds 需要删除的数据ID
+ * @return 结果
+ */
+ @Override
+ public int deleteJobLogByIds(Long[] logIds)
+ {
+ return jobLogMapper.deleteJobLogByIds(logIds);
+ }
+
+ /**
+ * 删除任务日志
+ *
+ * @param jobId 调度日志ID
+ */
+ @Override
+ public int deleteJobLogById(Long jobId)
+ {
+ return jobLogMapper.deleteJobLogById(jobId);
+ }
+
+ /**
+ * 清空任务日志
+ */
+ @Override
+ public void cleanJobLog()
+ {
+ jobLogMapper.cleanJobLog();
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java
new file mode 100644
index 0000000..78ebef8
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java
@@ -0,0 +1,261 @@
+package com.ruoyi.quartz.service.impl;
+
+import java.util.List;
+import jakarta.annotation.PostConstruct;
+import org.quartz.JobDataMap;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.constant.ScheduleConstants;
+import com.ruoyi.common.exception.job.TaskException;
+import com.ruoyi.quartz.domain.SysJob;
+import com.ruoyi.quartz.mapper.SysJobMapper;
+import com.ruoyi.quartz.service.ISysJobService;
+import com.ruoyi.quartz.util.CronUtils;
+import com.ruoyi.quartz.util.ScheduleUtils;
+
+/**
+ * 定时任务调度信息 服务层
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysJobServiceImpl implements ISysJobService
+{
+ @Autowired
+ private Scheduler scheduler;
+
+ @Autowired
+ private SysJobMapper jobMapper;
+
+ /**
+ * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
+ */
+ @PostConstruct
+ public void init() throws SchedulerException, TaskException
+ {
+ scheduler.clear();
+ List<SysJob> jobList = jobMapper.selectJobAll();
+ for (SysJob job : jobList)
+ {
+ ScheduleUtils.createScheduleJob(scheduler, job);
+ }
+ }
+
+ /**
+ * 获取quartz调度器的计划任务列表
+ *
+ * @param job 调度信息
+ * @return
+ */
+ @Override
+ public List<SysJob> selectJobList(SysJob job)
+ {
+ return jobMapper.selectJobList(job);
+ }
+
+ /**
+ * 通过调度任务ID查询调度信息
+ *
+ * @param jobId 调度任务ID
+ * @return 调度任务对象信息
+ */
+ @Override
+ public SysJob selectJobById(Long jobId)
+ {
+ return jobMapper.selectJobById(jobId);
+ }
+
+ /**
+ * 暂停任务
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int pauseJob(SysJob job) throws SchedulerException
+ {
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
+ int rows = jobMapper.updateJob(job);
+ if (rows > 0)
+ {
+ scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+ }
+ return rows;
+ }
+
+ /**
+ * 恢复任务
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int resumeJob(SysJob job) throws SchedulerException
+ {
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
+ int rows = jobMapper.updateJob(job);
+ if (rows > 0)
+ {
+ scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+ }
+ return rows;
+ }
+
+ /**
+ * 删除任务后,所对应的trigger也将被删除
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int deleteJob(SysJob job) throws SchedulerException
+ {
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ int rows = jobMapper.deleteJobById(jobId);
+ if (rows > 0)
+ {
+ scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+ }
+ return rows;
+ }
+
+ /**
+ * 批量删除调度信息
+ *
+ * @param jobIds 需要删除的任务ID
+ * @return 结果
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteJobByIds(Long[] jobIds) throws SchedulerException
+ {
+ for (Long jobId : jobIds)
+ {
+ SysJob job = jobMapper.selectJobById(jobId);
+ deleteJob(job);
+ }
+ }
+
+ /**
+ * 任务调度状态修改
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int changeStatus(SysJob job) throws SchedulerException
+ {
+ int rows = 0;
+ String status = job.getStatus();
+ if (ScheduleConstants.Status.NORMAL.getValue().equals(status))
+ {
+ rows = resumeJob(job);
+ }
+ else if (ScheduleConstants.Status.PAUSE.getValue().equals(status))
+ {
+ rows = pauseJob(job);
+ }
+ return rows;
+ }
+
+ /**
+ * 立即运行任务
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean run(SysJob job) throws SchedulerException
+ {
+ boolean result = false;
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ SysJob properties = selectJobById(job.getJobId());
+ // 参数
+ JobDataMap dataMap = new JobDataMap();
+ dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties);
+ JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
+ if (scheduler.checkExists(jobKey))
+ {
+ result = true;
+ scheduler.triggerJob(jobKey, dataMap);
+ }
+ return result;
+ }
+
+ /**
+ * 新增任务
+ *
+ * @param job 调度信息 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int insertJob(SysJob job) throws SchedulerException, TaskException
+ {
+ job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
+ int rows = jobMapper.insertJob(job);
+ if (rows > 0)
+ {
+ ScheduleUtils.createScheduleJob(scheduler, job);
+ }
+ return rows;
+ }
+
+ /**
+ * 更新任务的时间表达式
+ *
+ * @param job 调度信息
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int updateJob(SysJob job) throws SchedulerException, TaskException
+ {
+ SysJob properties = selectJobById(job.getJobId());
+ int rows = jobMapper.updateJob(job);
+ if (rows > 0)
+ {
+ updateSchedulerJob(job, properties.getJobGroup());
+ }
+ return rows;
+ }
+
+ /**
+ * 更新任务
+ *
+ * @param job 任务对象
+ * @param jobGroup 任务组名
+ */
+ public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException
+ {
+ Long jobId = job.getJobId();
+ // 判断是否存在
+ JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
+ if (scheduler.checkExists(jobKey))
+ {
+ // 防止创建时存在数据问题 先移除,然后在执行创建操作
+ scheduler.deleteJob(jobKey);
+ }
+ ScheduleUtils.createScheduleJob(scheduler, job);
+ }
+
+ /**
+ * 校验cron表达式是否有效
+ *
+ * @param cronExpression 表达式
+ * @return 结果
+ */
+ @Override
+ public boolean checkCronExpressionIsValid(String cronExpression)
+ {
+ return CronUtils.isValid(cronExpression);
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
new file mode 100644
index 0000000..853243b
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -0,0 +1,28 @@
+package com.ruoyi.quartz.task;
+
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 定时任务调度测试
+ *
+ * @author ruoyi
+ */
+@Component("ryTask")
+public class RyTask
+{
+ public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i)
+ {
+ System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
+ }
+
+ public void ryParams(String params)
+ {
+ System.out.println("执行有参方法:" + params);
+ }
+
+ public void ryNoParams()
+ {
+ System.out.println("执行无参方法");
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java
new file mode 100644
index 0000000..731a5eb
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java
@@ -0,0 +1,107 @@
+package com.ruoyi.quartz.util;
+
+import java.util.Date;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.ScheduleConstants;
+import com.ruoyi.common.utils.ExceptionUtil;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.bean.BeanUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.quartz.domain.SysJob;
+import com.ruoyi.quartz.domain.SysJobLog;
+import com.ruoyi.quartz.service.ISysJobLogService;
+
+/**
+ * 抽象quartz调用
+ *
+ * @author ruoyi
+ */
+public abstract class AbstractQuartzJob implements Job
+{
+ private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
+
+ /**
+ * 线程本地变量
+ */
+ private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();
+
+ @Override
+ public void execute(JobExecutionContext context) throws JobExecutionException
+ {
+ SysJob sysJob = new SysJob();
+ BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
+ try
+ {
+ before(context, sysJob);
+ if (sysJob != null)
+ {
+ doExecute(context, sysJob);
+ }
+ after(context, sysJob, null);
+ }
+ catch (Exception e)
+ {
+ log.error("任务执行异常 - :", e);
+ after(context, sysJob, e);
+ }
+ }
+
+ /**
+ * 执行前
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ */
+ protected void before(JobExecutionContext context, SysJob sysJob)
+ {
+ threadLocal.set(new Date());
+ }
+
+ /**
+ * 执行后
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ */
+ protected void after(JobExecutionContext context, SysJob sysJob, Exception e)
+ {
+ Date startTime = threadLocal.get();
+ threadLocal.remove();
+
+ final SysJobLog sysJobLog = new SysJobLog();
+ sysJobLog.setJobName(sysJob.getJobName());
+ sysJobLog.setJobGroup(sysJob.getJobGroup());
+ sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
+ sysJobLog.setStartTime(startTime);
+ sysJobLog.setStopTime(new Date());
+ long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
+ sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
+ if (e != null)
+ {
+ sysJobLog.setStatus(Constants.FAIL);
+ String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000);
+ sysJobLog.setExceptionInfo(errorMsg);
+ }
+ else
+ {
+ sysJobLog.setStatus(Constants.SUCCESS);
+ }
+
+ // 写入数据库当中
+ SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog);
+ }
+
+ /**
+ * 执行方法,由子类重载
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ * @throws Exception 执行过程中的异常
+ */
+ protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java
new file mode 100644
index 0000000..dd53839
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java
@@ -0,0 +1,63 @@
+package com.ruoyi.quartz.util;
+
+import java.text.ParseException;
+import java.util.Date;
+import org.quartz.CronExpression;
+
+/**
+ * cron表达式工具类
+ *
+ * @author ruoyi
+ *
+ */
+public class CronUtils
+{
+ /**
+ * 返回一个布尔值代表一个给定的Cron表达式的有效性
+ *
+ * @param cronExpression Cron表达式
+ * @return boolean 表达式是否有效
+ */
+ public static boolean isValid(String cronExpression)
+ {
+ return CronExpression.isValidExpression(cronExpression);
+ }
+
+ /**
+ * 返回一个字符串值,表示该消息无效Cron表达式给出有效性
+ *
+ * @param cronExpression Cron表达式
+ * @return String 无效时返回表达式错误描述,如果有效返回null
+ */
+ public static String getInvalidMessage(String cronExpression)
+ {
+ try
+ {
+ new CronExpression(cronExpression);
+ return null;
+ }
+ catch (ParseException pe)
+ {
+ return pe.getMessage();
+ }
+ }
+
+ /**
+ * 返回下一个执行时间根据给定的Cron表达式
+ *
+ * @param cronExpression Cron表达式
+ * @return Date 下次Cron表达式执行时间
+ */
+ public static Date getNextExecution(String cronExpression)
+ {
+ try
+ {
+ CronExpression cron = new CronExpression(cronExpression);
+ return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
+ }
+ catch (ParseException e)
+ {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java
new file mode 100644
index 0000000..e3dc62c
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java
@@ -0,0 +1,182 @@
+package com.ruoyi.quartz.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.LinkedList;
+import java.util.List;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 任务执行工具
+ *
+ * @author ruoyi
+ */
+public class JobInvokeUtil
+{
+ /**
+ * 执行方法
+ *
+ * @param sysJob 系统任务
+ */
+ public static void invokeMethod(SysJob sysJob) throws Exception
+ {
+ String invokeTarget = sysJob.getInvokeTarget();
+ String beanName = getBeanName(invokeTarget);
+ String methodName = getMethodName(invokeTarget);
+ List<Object[]> methodParams = getMethodParams(invokeTarget);
+
+ if (!isValidClassName(beanName))
+ {
+ Object bean = SpringUtils.getBean(beanName);
+ invokeMethod(bean, methodName, methodParams);
+ }
+ else
+ {
+ Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
+ invokeMethod(bean, methodName, methodParams);
+ }
+ }
+
+ /**
+ * 调用任务方法
+ *
+ * @param bean 目标对象
+ * @param methodName 方法名称
+ * @param methodParams 方法参数
+ */
+ private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException
+ {
+ if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)
+ {
+ Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
+ method.invoke(bean, getMethodParamsValue(methodParams));
+ }
+ else
+ {
+ Method method = bean.getClass().getMethod(methodName);
+ method.invoke(bean);
+ }
+ }
+
+ /**
+ * 校验是否为为class包名
+ *
+ * @param invokeTarget 名称
+ * @return true是 false否
+ */
+ public static boolean isValidClassName(String invokeTarget)
+ {
+ return StringUtils.countMatches(invokeTarget, ".") > 1;
+ }
+
+ /**
+ * 获取bean名称
+ *
+ * @param invokeTarget 目标字符串
+ * @return bean名称
+ */
+ public static String getBeanName(String invokeTarget)
+ {
+ String beanName = StringUtils.substringBefore(invokeTarget, "(");
+ return StringUtils.substringBeforeLast(beanName, ".");
+ }
+
+ /**
+ * 获取bean方法
+ *
+ * @param invokeTarget 目标字符串
+ * @return method方法
+ */
+ public static String getMethodName(String invokeTarget)
+ {
+ String methodName = StringUtils.substringBefore(invokeTarget, "(");
+ return StringUtils.substringAfterLast(methodName, ".");
+ }
+
+ /**
+ * 获取method方法参数相关列表
+ *
+ * @param invokeTarget 目标字符串
+ * @return method方法相关参数列表
+ */
+ public static List<Object[]> getMethodParams(String invokeTarget)
+ {
+ String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
+ if (StringUtils.isEmpty(methodStr))
+ {
+ return null;
+ }
+ String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)");
+ List<Object[]> classs = new LinkedList<>();
+ for (int i = 0; i < methodParams.length; i++)
+ {
+ String str = StringUtils.trimToEmpty(methodParams[i]);
+ // String字符串类型,以'或"开头
+ if (StringUtils.startsWithAny(str, "'", "\""))
+ {
+ classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class });
+ }
+ // boolean布尔类型,等于true或者false
+ else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str))
+ {
+ classs.add(new Object[] { Boolean.valueOf(str), Boolean.class });
+ }
+ // long长整形,以L结尾
+ else if (StringUtils.endsWith(str, "L"))
+ {
+ classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class });
+ }
+ // double浮点类型,以D结尾
+ else if (StringUtils.endsWith(str, "D"))
+ {
+ classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class });
+ }
+ // 其他类型归类为整形
+ else
+ {
+ classs.add(new Object[] { Integer.valueOf(str), Integer.class });
+ }
+ }
+ return classs;
+ }
+
+ /**
+ * 获取参数类型
+ *
+ * @param methodParams 参数相关列表
+ * @return 参数类型列表
+ */
+ public static Class<?>[] getMethodParamsType(List<Object[]> methodParams)
+ {
+ Class<?>[] classs = new Class<?>[methodParams.size()];
+ int index = 0;
+ for (Object[] os : methodParams)
+ {
+ classs[index] = (Class<?>) os[1];
+ index++;
+ }
+ return classs;
+ }
+
+ /**
+ * 获取参数值
+ *
+ * @param methodParams 参数相关列表
+ * @return 参数值列表
+ */
+ public static Object[] getMethodParamsValue(List<Object[]> methodParams)
+ {
+ Object[] classs = new Object[methodParams.size()];
+ int index = 0;
+ for (Object[] os : methodParams)
+ {
+ classs[index] = (Object) os[0];
+ index++;
+ }
+ return classs;
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java
new file mode 100644
index 0000000..5e13558
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java
@@ -0,0 +1,21 @@
+package com.ruoyi.quartz.util;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.JobExecutionContext;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 定时任务处理(禁止并发执行)
+ *
+ * @author ruoyi
+ *
+ */
+@DisallowConcurrentExecution
+public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob
+{
+ @Override
+ protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
+ {
+ JobInvokeUtil.invokeMethod(sysJob);
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java
new file mode 100644
index 0000000..e975326
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java
@@ -0,0 +1,19 @@
+package com.ruoyi.quartz.util;
+
+import org.quartz.JobExecutionContext;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 定时任务处理(允许并发执行)
+ *
+ * @author ruoyi
+ *
+ */
+public class QuartzJobExecution extends AbstractQuartzJob
+{
+ @Override
+ protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
+ {
+ JobInvokeUtil.invokeMethod(sysJob);
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java
new file mode 100644
index 0000000..21fedae
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java
@@ -0,0 +1,141 @@
+package com.ruoyi.quartz.util;
+
+import org.quartz.CronScheduleBuilder;
+import org.quartz.CronTrigger;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.TriggerBuilder;
+import org.quartz.TriggerKey;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.ScheduleConstants;
+import com.ruoyi.common.exception.job.TaskException;
+import com.ruoyi.common.exception.job.TaskException.Code;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.quartz.domain.SysJob;
+
+/**
+ * 定时任务工具类
+ *
+ * @author ruoyi
+ *
+ */
+public class ScheduleUtils
+{
+ /**
+ * 得到quartz任务类
+ *
+ * @param sysJob 执行计划
+ * @return 具体执行任务类
+ */
+ private static Class<? extends Job> getQuartzJobClass(SysJob sysJob)
+ {
+ boolean isConcurrent = "0".equals(sysJob.getConcurrent());
+ return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
+ }
+
+ /**
+ * 构建任务触发对象
+ */
+ public static TriggerKey getTriggerKey(Long jobId, String jobGroup)
+ {
+ return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+ }
+
+ /**
+ * 构建任务键对象
+ */
+ public static JobKey getJobKey(Long jobId, String jobGroup)
+ {
+ return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+ }
+
+ /**
+ * 创建定时任务
+ */
+ public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException
+ {
+ Class<? extends Job> jobClass = getQuartzJobClass(job);
+ // 构建job信息
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
+
+ // 表达式调度构建器
+ CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
+ cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
+
+ // 按新的cronExpression表达式构建一个新的trigger
+ CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
+ .withSchedule(cronScheduleBuilder).build();
+
+ // 放入参数,运行时的方法可以获取
+ jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
+
+ // 判断是否存在
+ if (scheduler.checkExists(getJobKey(jobId, jobGroup)))
+ {
+ // 防止创建时存在数据问题 先移除,然后在执行创建操作
+ scheduler.deleteJob(getJobKey(jobId, jobGroup));
+ }
+
+ // 判断任务是否过期
+ if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression())))
+ {
+ // 执行调度任务
+ scheduler.scheduleJob(jobDetail, trigger);
+ }
+
+ // 暂停任务
+ if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue()))
+ {
+ scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+ }
+ }
+
+ /**
+ * 设置定时任务策略
+ */
+ public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
+ throws TaskException
+ {
+ switch (job.getMisfirePolicy())
+ {
+ case ScheduleConstants.MISFIRE_DEFAULT:
+ return cb;
+ case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
+ return cb.withMisfireHandlingInstructionIgnoreMisfires();
+ case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
+ return cb.withMisfireHandlingInstructionFireAndProceed();
+ case ScheduleConstants.MISFIRE_DO_NOTHING:
+ return cb.withMisfireHandlingInstructionDoNothing();
+ default:
+ throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+ + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR);
+ }
+ }
+
+ /**
+ * 检查包名是否为白名单配置
+ *
+ * @param invokeTarget 目标字符串
+ * @return 结果
+ */
+ public static boolean whiteList(String invokeTarget)
+ {
+ String packageName = StringUtils.substringBefore(invokeTarget, "(");
+ int count = StringUtils.countMatches(packageName, ".");
+ if (count > 1)
+ {
+ return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);
+ }
+ Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
+ String beanPackageName = obj.getClass().getPackage().getName();
+ return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR)
+ && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR);
+ }
+}
diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml
new file mode 100644
index 0000000..ba1b683
--- /dev/null
+++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.quartz.mapper.SysJobLogMapper">
+
+ <resultMap type="SysJobLog" id="SysJobLogResult">
+ <id property="jobLogId" column="job_log_id" />
+ <result property="jobName" column="job_name" />
+ <result property="jobGroup" column="job_group" />
+ <result property="invokeTarget" column="invoke_target" />
+ <result property="jobMessage" column="job_message" />
+ <result property="status" column="status" />
+ <result property="exceptionInfo" column="exception_info" />
+ <result property="createTime" column="create_time" />
+ </resultMap>
+
+ <sql id="selectJobLogVo">
+ select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time
+ from sys_job_log
+ </sql>
+
+ <select id="selectJobLogList" parameterType="SysJobLog" resultMap="SysJobLogResult">
+ <include refid="selectJobLogVo"/>
+ <where>
+ <if test="jobName != null and jobName != ''">
+ AND job_name like concat('%', #{jobName}, '%')
+ </if>
+ <if test="jobGroup != null and jobGroup != ''">
+ AND job_group = #{jobGroup}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="invokeTarget != null and invokeTarget != ''">
+ AND invoke_target like concat('%', #{invokeTarget}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ and date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ </where>
+ order by create_time desc
+ </select>
+
+ <select id="selectJobLogAll" resultMap="SysJobLogResult">
+ <include refid="selectJobLogVo"/>
+ </select>
+
+ <select id="selectJobLogById" parameterType="Long" resultMap="SysJobLogResult">
+ <include refid="selectJobLogVo"/>
+ where job_log_id = #{jobLogId}
+ </select>
+
+ <delete id="deleteJobLogById" parameterType="Long">
+ delete from sys_job_log where job_log_id = #{jobLogId}
+ </delete>
+
+ <delete id="deleteJobLogByIds" parameterType="Long">
+ delete from sys_job_log where job_log_id in
+ <foreach collection="array" item="jobLogId" open="(" separator="," close=")">
+ #{jobLogId}
+ </foreach>
+ </delete>
+
+ <update id="cleanJobLog">
+ truncate table sys_job_log
+ </update>
+
+ <insert id="insertJobLog" parameterType="SysJobLog">
+ insert into sys_job_log(
+ <if test="jobLogId != null and jobLogId != 0">job_log_id,</if>
+ <if test="jobName != null and jobName != ''">job_name,</if>
+ <if test="jobGroup != null and jobGroup != ''">job_group,</if>
+ <if test="invokeTarget != null and invokeTarget != ''">invoke_target,</if>
+ <if test="jobMessage != null and jobMessage != ''">job_message,</if>
+ <if test="status != null and status != ''">status,</if>
+ <if test="exceptionInfo != null and exceptionInfo != ''">exception_info,</if>
+ create_time
+ )values(
+ <if test="jobLogId != null and jobLogId != 0">#{jobLogId},</if>
+ <if test="jobName != null and jobName != ''">#{jobName},</if>
+ <if test="jobGroup != null and jobGroup != ''">#{jobGroup},</if>
+ <if test="invokeTarget != null and invokeTarget != ''">#{invokeTarget},</if>
+ <if test="jobMessage != null and jobMessage != ''">#{jobMessage},</if>
+ <if test="status != null and status != ''">#{status},</if>
+ <if test="exceptionInfo != null and exceptionInfo != ''">#{exceptionInfo},</if>
+ sysdate()
+ )
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml
new file mode 100644
index 0000000..5605c44
--- /dev/null
+++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.quartz.mapper.SysJobMapper">
+
+ <resultMap type="SysJob" id="SysJobResult">
+ <id property="jobId" column="job_id" />
+ <result property="jobName" column="job_name" />
+ <result property="jobGroup" column="job_group" />
+ <result property="invokeTarget" column="invoke_target" />
+ <result property="cronExpression" column="cron_expression" />
+ <result property="misfirePolicy" column="misfire_policy" />
+ <result property="concurrent" column="concurrent" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectJobVo">
+ select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark
+ from sys_job
+ </sql>
+
+ <select id="selectJobList" parameterType="SysJob" resultMap="SysJobResult">
+ <include refid="selectJobVo"/>
+ <where>
+ <if test="jobName != null and jobName != ''">
+ AND job_name like concat('%', #{jobName}, '%')
+ </if>
+ <if test="jobGroup != null and jobGroup != ''">
+ AND job_group = #{jobGroup}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="invokeTarget != null and invokeTarget != ''">
+ AND invoke_target like concat('%', #{invokeTarget}, '%')
+ </if>
+ </where>
+ </select>
+
+ <select id="selectJobAll" resultMap="SysJobResult">
+ <include refid="selectJobVo"/>
+ </select>
+
+ <select id="selectJobById" parameterType="Long" resultMap="SysJobResult">
+ <include refid="selectJobVo"/>
+ where job_id = #{jobId}
+ </select>
+
+ <delete id="deleteJobById" parameterType="Long">
+ delete from sys_job where job_id = #{jobId}
+ </delete>
+
+ <delete id="deleteJobByIds" parameterType="Long">
+ delete from sys_job where job_id in
+ <foreach collection="array" item="jobId" open="(" separator="," close=")">
+ #{jobId}
+ </foreach>
+ </delete>
+
+ <update id="updateJob" parameterType="SysJob">
+ update sys_job
+ <set>
+ <if test="jobName != null and jobName != ''">job_name = #{jobName},</if>
+ <if test="jobGroup != null and jobGroup != ''">job_group = #{jobGroup},</if>
+ <if test="invokeTarget != null and invokeTarget != ''">invoke_target = #{invokeTarget},</if>
+ <if test="cronExpression != null and cronExpression != ''">cron_expression = #{cronExpression},</if>
+ <if test="misfirePolicy != null and misfirePolicy != ''">misfire_policy = #{misfirePolicy},</if>
+ <if test="concurrent != null and concurrent != ''">concurrent = #{concurrent},</if>
+ <if test="status !=null">status = #{status},</if>
+ <if test="remark != null and remark != ''">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where job_id = #{jobId}
+ </update>
+
+ <insert id="insertJob" parameterType="SysJob" useGeneratedKeys="true" keyProperty="jobId">
+ insert into sys_job(
+ <if test="jobId != null and jobId != 0">job_id,</if>
+ <if test="jobName != null and jobName != ''">job_name,</if>
+ <if test="jobGroup != null and jobGroup != ''">job_group,</if>
+ <if test="invokeTarget != null and invokeTarget != ''">invoke_target,</if>
+ <if test="cronExpression != null and cronExpression != ''">cron_expression,</if>
+ <if test="misfirePolicy != null and misfirePolicy != ''">misfire_policy,</if>
+ <if test="concurrent != null and concurrent != ''">concurrent,</if>
+ <if test="status != null and status != ''">status,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="jobId != null and jobId != 0">#{jobId},</if>
+ <if test="jobName != null and jobName != ''">#{jobName},</if>
+ <if test="jobGroup != null and jobGroup != ''">#{jobGroup},</if>
+ <if test="invokeTarget != null and invokeTarget != ''">#{invokeTarget},</if>
+ <if test="cronExpression != null and cronExpression != ''">#{cronExpression},</if>
+ <if test="misfirePolicy != null and misfirePolicy != ''">#{misfirePolicy},</if>
+ <if test="concurrent != null and concurrent != ''">#{concurrent},</if>
+ <if test="status != null and status != ''">#{status},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml
new file mode 100644
index 0000000..8f65273
--- /dev/null
+++ b/ruoyi-system/pom.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>ThunderHubServer</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-system</artifactId>
+
+ <description>
+ system系统模块
+ </description>
+
+ <dependencies>
+
+ <!-- 通用工具-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-common</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java
new file mode 100644
index 0000000..83f0703
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java
@@ -0,0 +1,81 @@
+package com.ruoyi.system.domain;
+
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 缓存信息
+ *
+ * @author ruoyi
+ */
+public class SysCache
+{
+ /** 缓存名称 */
+ private String cacheName = "";
+
+ /** 缓存键名 */
+ private String cacheKey = "";
+
+ /** 缓存内容 */
+ private String cacheValue = "";
+
+ /** 备注 */
+ private String remark = "";
+
+ public SysCache()
+ {
+
+ }
+
+ public SysCache(String cacheName, String remark)
+ {
+ this.cacheName = cacheName;
+ this.remark = remark;
+ }
+
+ public SysCache(String cacheName, String cacheKey, String cacheValue)
+ {
+ this.cacheName = StringUtils.replace(cacheName, ":", "");
+ this.cacheKey = StringUtils.replace(cacheKey, cacheName, "");
+ this.cacheValue = cacheValue;
+ }
+
+ public String getCacheName()
+ {
+ return cacheName;
+ }
+
+ public void setCacheName(String cacheName)
+ {
+ this.cacheName = cacheName;
+ }
+
+ public String getCacheKey()
+ {
+ return cacheKey;
+ }
+
+ public void setCacheKey(String cacheKey)
+ {
+ this.cacheKey = cacheKey;
+ }
+
+ public String getCacheValue()
+ {
+ return cacheValue;
+ }
+
+ public void setCacheValue(String cacheValue)
+ {
+ this.cacheValue = cacheValue;
+ }
+
+ public String getRemark()
+ {
+ return remark;
+ }
+
+ public void setRemark(String remark)
+ {
+ this.remark = remark;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java
new file mode 100644
index 0000000..cae07be
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java
@@ -0,0 +1,111 @@
+package com.ruoyi.system.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 参数配置表 sys_config
+ *
+ * @author ruoyi
+ */
+public class SysConfig extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 参数主键 */
+ @Excel(name = "参数主键", cellType = ColumnType.NUMERIC)
+ private Long configId;
+
+ /** 参数名称 */
+ @Excel(name = "参数名称")
+ private String configName;
+
+ /** 参数键名 */
+ @Excel(name = "参数键名")
+ private String configKey;
+
+ /** 参数键值 */
+ @Excel(name = "参数键值")
+ private String configValue;
+
+ /** 系统内置(Y是 N否) */
+ @Excel(name = "系统内置", readConverterExp = "Y=是,N=否")
+ private String configType;
+
+ public Long getConfigId()
+ {
+ return configId;
+ }
+
+ public void setConfigId(Long configId)
+ {
+ this.configId = configId;
+ }
+
+ @NotBlank(message = "参数名称不能为空")
+ @Size(min = 0, max = 100, message = "参数名称不能超过100个字符")
+ public String getConfigName()
+ {
+ return configName;
+ }
+
+ public void setConfigName(String configName)
+ {
+ this.configName = configName;
+ }
+
+ @NotBlank(message = "参数键名长度不能为空")
+ @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符")
+ public String getConfigKey()
+ {
+ return configKey;
+ }
+
+ public void setConfigKey(String configKey)
+ {
+ this.configKey = configKey;
+ }
+
+ @NotBlank(message = "参数键值不能为空")
+ @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符")
+ public String getConfigValue()
+ {
+ return configValue;
+ }
+
+ public void setConfigValue(String configValue)
+ {
+ this.configValue = configValue;
+ }
+
+ public String getConfigType()
+ {
+ return configType;
+ }
+
+ public void setConfigType(String configType)
+ {
+ this.configType = configType;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("configId", getConfigId())
+ .append("configName", getConfigName())
+ .append("configKey", getConfigKey())
+ .append("configValue", getConfigValue())
+ .append("configType", getConfigType())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java
new file mode 100644
index 0000000..7fdea30
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java
@@ -0,0 +1,144 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 系统访问记录表 sys_logininfor
+ *
+ * @author ruoyi
+ */
+public class SysLogininfor extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** ID */
+ @Excel(name = "序号", cellType = ColumnType.NUMERIC)
+ private Long infoId;
+
+ /** 用户账号 */
+ @Excel(name = "用户账号")
+ private String userName;
+
+ /** 登录状态 0成功 1失败 */
+ @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败")
+ private String status;
+
+ /** 登录IP地址 */
+ @Excel(name = "登录地址")
+ private String ipaddr;
+
+ /** 登录地点 */
+ @Excel(name = "登录地点")
+ private String loginLocation;
+
+ /** 浏览器类型 */
+ @Excel(name = "浏览器")
+ private String browser;
+
+ /** 操作系统 */
+ @Excel(name = "操作系统")
+ private String os;
+
+ /** 提示消息 */
+ @Excel(name = "提示消息")
+ private String msg;
+
+ /** 访问时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date loginTime;
+
+ public Long getInfoId()
+ {
+ return infoId;
+ }
+
+ public void setInfoId(Long infoId)
+ {
+ this.infoId = infoId;
+ }
+
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ public void setUserName(String userName)
+ {
+ this.userName = userName;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getIpaddr()
+ {
+ return ipaddr;
+ }
+
+ public void setIpaddr(String ipaddr)
+ {
+ this.ipaddr = ipaddr;
+ }
+
+ public String getLoginLocation()
+ {
+ return loginLocation;
+ }
+
+ public void setLoginLocation(String loginLocation)
+ {
+ this.loginLocation = loginLocation;
+ }
+
+ public String getBrowser()
+ {
+ return browser;
+ }
+
+ public void setBrowser(String browser)
+ {
+ this.browser = browser;
+ }
+
+ public String getOs()
+ {
+ return os;
+ }
+
+ public void setOs(String os)
+ {
+ this.os = os;
+ }
+
+ public String getMsg()
+ {
+ return msg;
+ }
+
+ public void setMsg(String msg)
+ {
+ this.msg = msg;
+ }
+
+ public Date getLoginTime()
+ {
+ return loginTime;
+ }
+
+ public void setLoginTime(Date loginTime)
+ {
+ this.loginTime = loginTime;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java
new file mode 100644
index 0000000..8603ad8
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java
@@ -0,0 +1,102 @@
+package com.ruoyi.system.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.xss.Xss;
+
+/**
+ * 通知公告表 sys_notice
+ *
+ * @author ruoyi
+ */
+public class SysNotice extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 公告ID */
+ private Long noticeId;
+
+ /** 公告标题 */
+ private String noticeTitle;
+
+ /** 公告类型(1通知 2公告) */
+ private String noticeType;
+
+ /** 公告内容 */
+ private String noticeContent;
+
+ /** 公告状态(0正常 1关闭) */
+ private String status;
+
+ public Long getNoticeId()
+ {
+ return noticeId;
+ }
+
+ public void setNoticeId(Long noticeId)
+ {
+ this.noticeId = noticeId;
+ }
+
+ public void setNoticeTitle(String noticeTitle)
+ {
+ this.noticeTitle = noticeTitle;
+ }
+
+ @Xss(message = "公告标题不能包含脚本字符")
+ @NotBlank(message = "公告标题不能为空")
+ @Size(min = 0, max = 50, message = "公告标题不能超过50个字符")
+ public String getNoticeTitle()
+ {
+ return noticeTitle;
+ }
+
+ public void setNoticeType(String noticeType)
+ {
+ this.noticeType = noticeType;
+ }
+
+ public String getNoticeType()
+ {
+ return noticeType;
+ }
+
+ public void setNoticeContent(String noticeContent)
+ {
+ this.noticeContent = noticeContent;
+ }
+
+ public String getNoticeContent()
+ {
+ return noticeContent;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("noticeId", getNoticeId())
+ .append("noticeTitle", getNoticeTitle())
+ .append("noticeType", getNoticeType())
+ .append("noticeContent", getNoticeContent())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java
new file mode 100644
index 0000000..f6761df
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java
@@ -0,0 +1,269 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 操作日志记录表 oper_log
+ *
+ * @author ruoyi
+ */
+public class SysOperLog extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 日志主键 */
+ @Excel(name = "操作序号", cellType = ColumnType.NUMERIC)
+ private Long operId;
+
+ /** 操作模块 */
+ @Excel(name = "操作模块")
+ private String title;
+
+ /** 业务类型(0其它 1新增 2修改 3删除) */
+ @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据")
+ private Integer businessType;
+
+ /** 业务类型数组 */
+ private Integer[] businessTypes;
+
+ /** 请求方法 */
+ @Excel(name = "请求方法")
+ private String method;
+
+ /** 请求方式 */
+ @Excel(name = "请求方式")
+ private String requestMethod;
+
+ /** 操作类别(0其它 1后台用户 2手机端用户) */
+ @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户")
+ private Integer operatorType;
+
+ /** 操作人员 */
+ @Excel(name = "操作人员")
+ private String operName;
+
+ /** 部门名称 */
+ @Excel(name = "部门名称")
+ private String deptName;
+
+ /** 请求url */
+ @Excel(name = "请求地址")
+ private String operUrl;
+
+ /** 操作地址 */
+ @Excel(name = "操作地址")
+ private String operIp;
+
+ /** 操作地点 */
+ @Excel(name = "操作地点")
+ private String operLocation;
+
+ /** 请求参数 */
+ @Excel(name = "请求参数")
+ private String operParam;
+
+ /** 返回参数 */
+ @Excel(name = "返回参数")
+ private String jsonResult;
+
+ /** 操作状态(0正常 1异常) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=异常")
+ private Integer status;
+
+ /** 错误消息 */
+ @Excel(name = "错误消息")
+ private String errorMsg;
+
+ /** 操作时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date operTime;
+
+ /** 消耗时间 */
+ @Excel(name = "消耗时间", suffix = "毫秒")
+ private Long costTime;
+
+ public Long getOperId()
+ {
+ return operId;
+ }
+
+ public void setOperId(Long operId)
+ {
+ this.operId = operId;
+ }
+
+ public String getTitle()
+ {
+ return title;
+ }
+
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public Integer getBusinessType()
+ {
+ return businessType;
+ }
+
+ public void setBusinessType(Integer businessType)
+ {
+ this.businessType = businessType;
+ }
+
+ public Integer[] getBusinessTypes()
+ {
+ return businessTypes;
+ }
+
+ public void setBusinessTypes(Integer[] businessTypes)
+ {
+ this.businessTypes = businessTypes;
+ }
+
+ public String getMethod()
+ {
+ return method;
+ }
+
+ public void setMethod(String method)
+ {
+ this.method = method;
+ }
+
+ public String getRequestMethod()
+ {
+ return requestMethod;
+ }
+
+ public void setRequestMethod(String requestMethod)
+ {
+ this.requestMethod = requestMethod;
+ }
+
+ public Integer getOperatorType()
+ {
+ return operatorType;
+ }
+
+ public void setOperatorType(Integer operatorType)
+ {
+ this.operatorType = operatorType;
+ }
+
+ public String getOperName()
+ {
+ return operName;
+ }
+
+ public void setOperName(String operName)
+ {
+ this.operName = operName;
+ }
+
+ public String getDeptName()
+ {
+ return deptName;
+ }
+
+ public void setDeptName(String deptName)
+ {
+ this.deptName = deptName;
+ }
+
+ public String getOperUrl()
+ {
+ return operUrl;
+ }
+
+ public void setOperUrl(String operUrl)
+ {
+ this.operUrl = operUrl;
+ }
+
+ public String getOperIp()
+ {
+ return operIp;
+ }
+
+ public void setOperIp(String operIp)
+ {
+ this.operIp = operIp;
+ }
+
+ public String getOperLocation()
+ {
+ return operLocation;
+ }
+
+ public void setOperLocation(String operLocation)
+ {
+ this.operLocation = operLocation;
+ }
+
+ public String getOperParam()
+ {
+ return operParam;
+ }
+
+ public void setOperParam(String operParam)
+ {
+ this.operParam = operParam;
+ }
+
+ public String getJsonResult()
+ {
+ return jsonResult;
+ }
+
+ public void setJsonResult(String jsonResult)
+ {
+ this.jsonResult = jsonResult;
+ }
+
+ public Integer getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(Integer status)
+ {
+ this.status = status;
+ }
+
+ public String getErrorMsg()
+ {
+ return errorMsg;
+ }
+
+ public void setErrorMsg(String errorMsg)
+ {
+ this.errorMsg = errorMsg;
+ }
+
+ public Date getOperTime()
+ {
+ return operTime;
+ }
+
+ public void setOperTime(Date operTime)
+ {
+ this.operTime = operTime;
+ }
+
+ public Long getCostTime()
+ {
+ return costTime;
+ }
+
+ public void setCostTime(Long costTime)
+ {
+ this.costTime = costTime;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java
new file mode 100644
index 0000000..62dbd47
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java
@@ -0,0 +1,124 @@
+package com.ruoyi.system.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 岗位表 sys_post
+ *
+ * @author ruoyi
+ */
+public class SysPost extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 岗位序号 */
+ @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC)
+ private Long postId;
+
+ /** 岗位编码 */
+ @Excel(name = "岗位编码")
+ private String postCode;
+
+ /** 岗位名称 */
+ @Excel(name = "岗位名称")
+ private String postName;
+
+ /** 岗位排序 */
+ @Excel(name = "岗位排序")
+ private Integer postSort;
+
+ /** 状态(0正常 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /** 用户是否存在此岗位标识 默认不存在 */
+ private boolean flag = false;
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ @NotBlank(message = "岗位编码不能为空")
+ @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符")
+ public String getPostCode()
+ {
+ return postCode;
+ }
+
+ public void setPostCode(String postCode)
+ {
+ this.postCode = postCode;
+ }
+
+ @NotBlank(message = "岗位名称不能为空")
+ @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符")
+ public String getPostName()
+ {
+ return postName;
+ }
+
+ public void setPostName(String postName)
+ {
+ this.postName = postName;
+ }
+
+ @NotNull(message = "显示顺序不能为空")
+ public Integer getPostSort()
+ {
+ return postSort;
+ }
+
+ public void setPostSort(Integer postSort)
+ {
+ this.postSort = postSort;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public boolean isFlag()
+ {
+ return flag;
+ }
+
+ public void setFlag(boolean flag)
+ {
+ this.flag = flag;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("postId", getPostId())
+ .append("postCode", getPostCode())
+ .append("postName", getPostName())
+ .append("postSort", getPostSort())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java
new file mode 100644
index 0000000..47b21bf
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java
@@ -0,0 +1,46 @@
+package com.ruoyi.system.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 角色和部门关联 sys_role_dept
+ *
+ * @author ruoyi
+ */
+public class SysRoleDept
+{
+ /** 角色ID */
+ private Long roleId;
+
+ /** 部门ID */
+ private Long deptId;
+
+ public Long getRoleId()
+ {
+ return roleId;
+ }
+
+ public void setRoleId(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ public Long getDeptId()
+ {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId)
+ {
+ this.deptId = deptId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("roleId", getRoleId())
+ .append("deptId", getDeptId())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java
new file mode 100644
index 0000000..de10a74
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java
@@ -0,0 +1,46 @@
+package com.ruoyi.system.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 角色和菜单关联 sys_role_menu
+ *
+ * @author ruoyi
+ */
+public class SysRoleMenu
+{
+ /** 角色ID */
+ private Long roleId;
+
+ /** 菜单ID */
+ private Long menuId;
+
+ public Long getRoleId()
+ {
+ return roleId;
+ }
+
+ public void setRoleId(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ public Long getMenuId()
+ {
+ return menuId;
+ }
+
+ public void setMenuId(Long menuId)
+ {
+ this.menuId = menuId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("roleId", getRoleId())
+ .append("menuId", getMenuId())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java
new file mode 100644
index 0000000..2bbd318
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java
@@ -0,0 +1,113 @@
+package com.ruoyi.system.domain;
+
+/**
+ * 当前在线会话
+ *
+ * @author ruoyi
+ */
+public class SysUserOnline
+{
+ /** 会话编号 */
+ private String tokenId;
+
+ /** 部门名称 */
+ private String deptName;
+
+ /** 用户名称 */
+ private String userName;
+
+ /** 登录IP地址 */
+ private String ipaddr;
+
+ /** 登录地址 */
+ private String loginLocation;
+
+ /** 浏览器类型 */
+ private String browser;
+
+ /** 操作系统 */
+ private String os;
+
+ /** 登录时间 */
+ private Long loginTime;
+
+ public String getTokenId()
+ {
+ return tokenId;
+ }
+
+ public void setTokenId(String tokenId)
+ {
+ this.tokenId = tokenId;
+ }
+
+ public String getDeptName()
+ {
+ return deptName;
+ }
+
+ public void setDeptName(String deptName)
+ {
+ this.deptName = deptName;
+ }
+
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ public void setUserName(String userName)
+ {
+ this.userName = userName;
+ }
+
+ public String getIpaddr()
+ {
+ return ipaddr;
+ }
+
+ public void setIpaddr(String ipaddr)
+ {
+ this.ipaddr = ipaddr;
+ }
+
+ public String getLoginLocation()
+ {
+ return loginLocation;
+ }
+
+ public void setLoginLocation(String loginLocation)
+ {
+ this.loginLocation = loginLocation;
+ }
+
+ public String getBrowser()
+ {
+ return browser;
+ }
+
+ public void setBrowser(String browser)
+ {
+ this.browser = browser;
+ }
+
+ public String getOs()
+ {
+ return os;
+ }
+
+ public void setOs(String os)
+ {
+ this.os = os;
+ }
+
+ public Long getLoginTime()
+ {
+ return loginTime;
+ }
+
+ public void setLoginTime(Long loginTime)
+ {
+ this.loginTime = loginTime;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java
new file mode 100644
index 0000000..6e8c416
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java
@@ -0,0 +1,46 @@
+package com.ruoyi.system.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 用户和岗位关联 sys_user_post
+ *
+ * @author ruoyi
+ */
+public class SysUserPost
+{
+ /** 用户ID */
+ private Long userId;
+
+ /** 岗位ID */
+ private Long postId;
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getPostId()
+ {
+ return postId;
+ }
+
+ public void setPostId(Long postId)
+ {
+ this.postId = postId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("userId", getUserId())
+ .append("postId", getPostId())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java
new file mode 100644
index 0000000..4d15810
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java
@@ -0,0 +1,46 @@
+package com.ruoyi.system.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 用户和角色关联 sys_user_role
+ *
+ * @author ruoyi
+ */
+public class SysUserRole
+{
+ /** 用户ID */
+ private Long userId;
+
+ /** 角色ID */
+ private Long roleId;
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getRoleId()
+ {
+ return roleId;
+ }
+
+ public void setRoleId(Long roleId)
+ {
+ this.roleId = roleId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("userId", getUserId())
+ .append("roleId", getRoleId())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java
new file mode 100644
index 0000000..a5d5fdc
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java
@@ -0,0 +1,106 @@
+package com.ruoyi.system.domain.vo;
+
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 路由显示信息
+ *
+ * @author ruoyi
+ */
+public class MetaVo
+{
+ /**
+ * 设置该路由在侧边栏和面包屑中展示的名字
+ */
+ private String title;
+
+ /**
+ * 设置该路由的图标,对应路径src/assets/icons/svg
+ */
+ private String icon;
+
+ /**
+ * 设置为true,则不会被 <keep-alive>缓存
+ */
+ private boolean noCache;
+
+ /**
+ * 内链地址(http(s)://开头)
+ */
+ private String link;
+
+ public MetaVo()
+ {
+ }
+
+ public MetaVo(String title, String icon)
+ {
+ this.title = title;
+ this.icon = icon;
+ }
+
+ public MetaVo(String title, String icon, boolean noCache)
+ {
+ this.title = title;
+ this.icon = icon;
+ this.noCache = noCache;
+ }
+
+ public MetaVo(String title, String icon, String link)
+ {
+ this.title = title;
+ this.icon = icon;
+ this.link = link;
+ }
+
+ public MetaVo(String title, String icon, boolean noCache, String link)
+ {
+ this.title = title;
+ this.icon = icon;
+ this.noCache = noCache;
+ if (StringUtils.ishttp(link))
+ {
+ this.link = link;
+ }
+ }
+
+ public boolean isNoCache()
+ {
+ return noCache;
+ }
+
+ public void setNoCache(boolean noCache)
+ {
+ this.noCache = noCache;
+ }
+
+ public String getTitle()
+ {
+ return title;
+ }
+
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public String getIcon()
+ {
+ return icon;
+ }
+
+ public void setIcon(String icon)
+ {
+ this.icon = icon;
+ }
+
+ public String getLink()
+ {
+ return link;
+ }
+
+ public void setLink(String link)
+ {
+ this.link = link;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java
new file mode 100644
index 0000000..afff8c9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java
@@ -0,0 +1,148 @@
+package com.ruoyi.system.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import java.util.List;
+
+/**
+ * 路由配置信息
+ *
+ * @author ruoyi
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class RouterVo
+{
+ /**
+ * 路由名字
+ */
+ private String name;
+
+ /**
+ * 路由地址
+ */
+ private String path;
+
+ /**
+ * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
+ */
+ private boolean hidden;
+
+ /**
+ * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ */
+ private String redirect;
+
+ /**
+ * 组件地址
+ */
+ private String component;
+
+ /**
+ * 路由参数:如 {"id": 1, "name": "ry"}
+ */
+ private String query;
+
+ /**
+ * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ */
+ private Boolean alwaysShow;
+
+ /**
+ * 其他元素
+ */
+ private MetaVo meta;
+
+ /**
+ * 子路由
+ */
+ private List<RouterVo> children;
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getPath()
+ {
+ return path;
+ }
+
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+ public boolean getHidden()
+ {
+ return hidden;
+ }
+
+ public void setHidden(boolean hidden)
+ {
+ this.hidden = hidden;
+ }
+
+ public String getRedirect()
+ {
+ return redirect;
+ }
+
+ public void setRedirect(String redirect)
+ {
+ this.redirect = redirect;
+ }
+
+ public String getComponent()
+ {
+ return component;
+ }
+
+ public void setComponent(String component)
+ {
+ this.component = component;
+ }
+
+ public String getQuery()
+ {
+ return query;
+ }
+
+ public void setQuery(String query)
+ {
+ this.query = query;
+ }
+
+ public Boolean getAlwaysShow()
+ {
+ return alwaysShow;
+ }
+
+ public void setAlwaysShow(Boolean alwaysShow)
+ {
+ this.alwaysShow = alwaysShow;
+ }
+
+ public MetaVo getMeta()
+ {
+ return meta;
+ }
+
+ public void setMeta(MetaVo meta)
+ {
+ this.meta = meta;
+ }
+
+ public List<RouterVo> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<RouterVo> children)
+ {
+ this.children = children;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java
new file mode 100644
index 0000000..13d49d6
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java
@@ -0,0 +1,76 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysConfig;
+
+/**
+ * 参数配置 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysConfigMapper
+{
+ /**
+ * 查询参数配置信息
+ *
+ * @param config 参数配置信息
+ * @return 参数配置信息
+ */
+ public SysConfig selectConfig(SysConfig config);
+
+ /**
+ * 通过ID查询配置
+ *
+ * @param configId 参数ID
+ * @return 参数配置信息
+ */
+ public SysConfig selectConfigById(Long configId);
+
+ /**
+ * 查询参数配置列表
+ *
+ * @param config 参数配置信息
+ * @return 参数配置集合
+ */
+ public List<SysConfig> selectConfigList(SysConfig config);
+
+ /**
+ * 根据键名查询参数配置信息
+ *
+ * @param configKey 参数键名
+ * @return 参数配置信息
+ */
+ public SysConfig checkConfigKeyUnique(String configKey);
+
+ /**
+ * 新增参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ public int insertConfig(SysConfig config);
+
+ /**
+ * 修改参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ public int updateConfig(SysConfig config);
+
+ /**
+ * 删除参数配置
+ *
+ * @param configId 参数ID
+ * @return 结果
+ */
+ public int deleteConfigById(Long configId);
+
+ /**
+ * 批量删除参数信息
+ *
+ * @param configIds 需要删除的参数ID
+ * @return 结果
+ */
+ public int deleteConfigByIds(Long[] configIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java
new file mode 100644
index 0000000..384a9b6
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java
@@ -0,0 +1,118 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.common.core.domain.entity.SysDept;
+
+/**
+ * 部门管理 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysDeptMapper
+{
+ /**
+ * 查询部门管理数据
+ *
+ * @param dept 部门信息
+ * @return 部门信息集合
+ */
+ public List<SysDept> selectDeptList(SysDept dept);
+
+ /**
+ * 根据角色ID查询部门树信息
+ *
+ * @param roleId 角色ID
+ * @param deptCheckStrictly 部门树选择项是否关联显示
+ * @return 选中部门列表
+ */
+ public List<Long> selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly);
+
+ /**
+ * 根据部门ID查询信息
+ *
+ * @param deptId 部门ID
+ * @return 部门信息
+ */
+ public SysDept selectDeptById(Long deptId);
+
+ /**
+ * 根据ID查询所有子部门
+ *
+ * @param deptId 部门ID
+ * @return 部门列表
+ */
+ public List<SysDept> selectChildrenDeptById(Long deptId);
+
+ /**
+ * 根据ID查询所有子部门(正常状态)
+ *
+ * @param deptId 部门ID
+ * @return 子部门数
+ */
+ public int selectNormalChildrenDeptById(Long deptId);
+
+ /**
+ * 是否存在子节点
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public int hasChildByDeptId(Long deptId);
+
+ /**
+ * 查询部门是否存在用户
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public int checkDeptExistUser(Long deptId);
+
+ /**
+ * 校验部门名称是否唯一
+ *
+ * @param deptName 部门名称
+ * @param parentId 父部门ID
+ * @return 结果
+ */
+ public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId);
+
+ /**
+ * 新增部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ public int insertDept(SysDept dept);
+
+ /**
+ * 修改部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ public int updateDept(SysDept dept);
+
+ /**
+ * 修改所在部门正常状态
+ *
+ * @param deptIds 部门ID组
+ */
+ public void updateDeptStatusNormal(Long[] deptIds);
+
+ /**
+ * 修改子元素关系
+ *
+ * @param depts 子元素
+ * @return 结果
+ */
+ public int updateDeptChildren(@Param("depts") List<SysDept> depts);
+
+ /**
+ * 删除部门管理信息
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public int deleteDeptById(Long deptId);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java
new file mode 100644
index 0000000..a341f1e
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java
@@ -0,0 +1,95 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+
+/**
+ * 字典表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysDictDataMapper
+{
+ /**
+ * 根据条件分页查询字典数据
+ *
+ * @param dictData 字典数据信息
+ * @return 字典数据集合信息
+ */
+ public List<SysDictData> selectDictDataList(SysDictData dictData);
+
+ /**
+ * 根据字典类型查询字典数据
+ *
+ * @param dictType 字典类型
+ * @return 字典数据集合信息
+ */
+ public List<SysDictData> selectDictDataByType(String dictType);
+
+ /**
+ * 根据字典类型和字典键值查询字典数据信息
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典键值
+ * @return 字典标签
+ */
+ public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue);
+
+ /**
+ * 根据字典数据ID查询信息
+ *
+ * @param dictCode 字典数据ID
+ * @return 字典数据
+ */
+ public SysDictData selectDictDataById(Long dictCode);
+
+ /**
+ * 查询字典数据
+ *
+ * @param dictType 字典类型
+ * @return 字典数据
+ */
+ public int countDictDataByType(String dictType);
+
+ /**
+ * 通过字典ID删除字典数据信息
+ *
+ * @param dictCode 字典数据ID
+ * @return 结果
+ */
+ public int deleteDictDataById(Long dictCode);
+
+ /**
+ * 批量删除字典数据信息
+ *
+ * @param dictCodes 需要删除的字典数据ID
+ * @return 结果
+ */
+ public int deleteDictDataByIds(Long[] dictCodes);
+
+ /**
+ * 新增字典数据信息
+ *
+ * @param dictData 字典数据信息
+ * @return 结果
+ */
+ public int insertDictData(SysDictData dictData);
+
+ /**
+ * 修改字典数据信息
+ *
+ * @param dictData 字典数据信息
+ * @return 结果
+ */
+ public int updateDictData(SysDictData dictData);
+
+ /**
+ * 同步修改字典类型
+ *
+ * @param oldDictType 旧字典类型
+ * @param newDictType 新旧字典类型
+ * @return 结果
+ */
+ public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java
new file mode 100644
index 0000000..5fb48fb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java
@@ -0,0 +1,83 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.entity.SysDictType;
+
+/**
+ * 字典表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysDictTypeMapper
+{
+ /**
+ * 根据条件分页查询字典类型
+ *
+ * @param dictType 字典类型信息
+ * @return 字典类型集合信息
+ */
+ public List<SysDictType> selectDictTypeList(SysDictType dictType);
+
+ /**
+ * 根据所有字典类型
+ *
+ * @return 字典类型集合信息
+ */
+ public List<SysDictType> selectDictTypeAll();
+
+ /**
+ * 根据字典类型ID查询信息
+ *
+ * @param dictId 字典类型ID
+ * @return 字典类型
+ */
+ public SysDictType selectDictTypeById(Long dictId);
+
+ /**
+ * 根据字典类型查询信息
+ *
+ * @param dictType 字典类型
+ * @return 字典类型
+ */
+ public SysDictType selectDictTypeByType(String dictType);
+
+ /**
+ * 通过字典ID删除字典信息
+ *
+ * @param dictId 字典ID
+ * @return 结果
+ */
+ public int deleteDictTypeById(Long dictId);
+
+ /**
+ * 批量删除字典类型信息
+ *
+ * @param dictIds 需要删除的字典ID
+ * @return 结果
+ */
+ public int deleteDictTypeByIds(Long[] dictIds);
+
+ /**
+ * 新增字典类型信息
+ *
+ * @param dictType 字典类型信息
+ * @return 结果
+ */
+ public int insertDictType(SysDictType dictType);
+
+ /**
+ * 修改字典类型信息
+ *
+ * @param dictType 字典类型信息
+ * @return 结果
+ */
+ public int updateDictType(SysDictType dictType);
+
+ /**
+ * 校验字典类型称是否唯一
+ *
+ * @param dictType 字典类型
+ * @return 结果
+ */
+ public SysDictType checkDictTypeUnique(String dictType);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java
new file mode 100644
index 0000000..629866f
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java
@@ -0,0 +1,42 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysLogininfor;
+
+/**
+ * 系统访问日志情况信息 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysLogininforMapper
+{
+ /**
+ * 新增系统登录日志
+ *
+ * @param logininfor 访问日志对象
+ */
+ public void insertLogininfor(SysLogininfor logininfor);
+
+ /**
+ * 查询系统登录日志集合
+ *
+ * @param logininfor 访问日志对象
+ * @return 登录记录集合
+ */
+ public List<SysLogininfor> selectLogininforList(SysLogininfor logininfor);
+
+ /**
+ * 批量删除系统登录日志
+ *
+ * @param infoIds 需要删除的登录日志ID
+ * @return 结果
+ */
+ public int deleteLogininforByIds(Long[] infoIds);
+
+ /**
+ * 清空系统登录日志
+ *
+ * @return 结果
+ */
+ public int cleanLogininfor();
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java
new file mode 100644
index 0000000..99c0c50
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java
@@ -0,0 +1,125 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+
+/**
+ * 菜单表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysMenuMapper
+{
+ /**
+ * 查询系统菜单列表
+ *
+ * @param menu 菜单信息
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuList(SysMenu menu);
+
+ /**
+ * 根据用户所有权限
+ *
+ * @return 权限列表
+ */
+ public List<String> selectMenuPerms();
+
+ /**
+ * 根据用户查询系统菜单列表
+ *
+ * @param menu 菜单信息
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuListByUserId(SysMenu menu);
+
+ /**
+ * 根据角色ID查询权限
+ *
+ * @param roleId 角色ID
+ * @return 权限列表
+ */
+ public List<String> selectMenuPermsByRoleId(Long roleId);
+
+ /**
+ * 根据用户ID查询权限
+ *
+ * @param userId 用户ID
+ * @return 权限列表
+ */
+ public List<String> selectMenuPermsByUserId(Long userId);
+
+ /**
+ * 根据用户ID查询菜单
+ *
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuTreeAll();
+
+ /**
+ * 根据用户ID查询菜单
+ *
+ * @param userId 用户ID
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuTreeByUserId(Long userId);
+
+ /**
+ * 根据角色ID查询菜单树信息
+ *
+ * @param roleId 角色ID
+ * @param menuCheckStrictly 菜单树选择项是否关联显示
+ * @return 选中菜单列表
+ */
+ public List<Long> selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly);
+
+ /**
+ * 根据菜单ID查询信息
+ *
+ * @param menuId 菜单ID
+ * @return 菜单信息
+ */
+ public SysMenu selectMenuById(Long menuId);
+
+ /**
+ * 是否存在菜单子节点
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ public int hasChildByMenuId(Long menuId);
+
+ /**
+ * 新增菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public int insertMenu(SysMenu menu);
+
+ /**
+ * 修改菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public int updateMenu(SysMenu menu);
+
+ /**
+ * 删除菜单管理信息
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ public int deleteMenuById(Long menuId);
+
+ /**
+ * 校验菜单名称是否唯一
+ *
+ * @param menuName 菜单名称
+ * @param parentId 父菜单ID
+ * @return 结果
+ */
+ public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java
new file mode 100644
index 0000000..c34f0a2
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java
@@ -0,0 +1,60 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysNotice;
+
+/**
+ * 通知公告表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysNoticeMapper
+{
+ /**
+ * 查询公告信息
+ *
+ * @param noticeId 公告ID
+ * @return 公告信息
+ */
+ public SysNotice selectNoticeById(Long noticeId);
+
+ /**
+ * 查询公告列表
+ *
+ * @param notice 公告信息
+ * @return 公告集合
+ */
+ public List<SysNotice> selectNoticeList(SysNotice notice);
+
+ /**
+ * 新增公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ public int insertNotice(SysNotice notice);
+
+ /**
+ * 修改公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ public int updateNotice(SysNotice notice);
+
+ /**
+ * 批量删除公告
+ *
+ * @param noticeId 公告ID
+ * @return 结果
+ */
+ public int deleteNoticeById(Long noticeId);
+
+ /**
+ * 批量删除公告信息
+ *
+ * @param noticeIds 需要删除的公告ID
+ * @return 结果
+ */
+ public int deleteNoticeByIds(Long[] noticeIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java
new file mode 100644
index 0000000..2ae6457
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java
@@ -0,0 +1,48 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysOperLog;
+
+/**
+ * 操作日志 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysOperLogMapper
+{
+ /**
+ * 新增操作日志
+ *
+ * @param operLog 操作日志对象
+ */
+ public void insertOperlog(SysOperLog operLog);
+
+ /**
+ * 查询系统操作日志集合
+ *
+ * @param operLog 操作日志对象
+ * @return 操作日志集合
+ */
+ public List<SysOperLog> selectOperLogList(SysOperLog operLog);
+
+ /**
+ * 批量删除系统操作日志
+ *
+ * @param operIds 需要删除的操作日志ID
+ * @return 结果
+ */
+ public int deleteOperLogByIds(Long[] operIds);
+
+ /**
+ * 查询操作日志详细
+ *
+ * @param operId 操作ID
+ * @return 操作日志对象
+ */
+ public SysOperLog selectOperLogById(Long operId);
+
+ /**
+ * 清空操作日志
+ */
+ public void cleanOperLog();
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java
new file mode 100644
index 0000000..19be227
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java
@@ -0,0 +1,99 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysPost;
+
+/**
+ * 岗位信息 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysPostMapper
+{
+ /**
+ * 查询岗位数据集合
+ *
+ * @param post 岗位信息
+ * @return 岗位数据集合
+ */
+ public List<SysPost> selectPostList(SysPost post);
+
+ /**
+ * 查询所有岗位
+ *
+ * @return 岗位列表
+ */
+ public List<SysPost> selectPostAll();
+
+ /**
+ * 通过岗位ID查询岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 角色对象信息
+ */
+ public SysPost selectPostById(Long postId);
+
+ /**
+ * 根据用户ID获取岗位选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中岗位ID列表
+ */
+ public List<Long> selectPostListByUserId(Long userId);
+
+ /**
+ * 查询用户所属岗位组
+ *
+ * @param userName 用户名
+ * @return 结果
+ */
+ public List<SysPost> selectPostsByUserName(String userName);
+
+ /**
+ * 删除岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ public int deletePostById(Long postId);
+
+ /**
+ * 批量删除岗位信息
+ *
+ * @param postIds 需要删除的岗位ID
+ * @return 结果
+ */
+ public int deletePostByIds(Long[] postIds);
+
+ /**
+ * 修改岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public int updatePost(SysPost post);
+
+ /**
+ * 新增岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public int insertPost(SysPost post);
+
+ /**
+ * 校验岗位名称
+ *
+ * @param postName 岗位名称
+ * @return 结果
+ */
+ public SysPost checkPostNameUnique(String postName);
+
+ /**
+ * 校验岗位编码
+ *
+ * @param postCode 岗位编码
+ * @return 结果
+ */
+ public SysPost checkPostCodeUnique(String postCode);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java
new file mode 100644
index 0000000..f9d3a2f
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java
@@ -0,0 +1,44 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysRoleDept;
+
+/**
+ * 角色与部门关联表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysRoleDeptMapper
+{
+ /**
+ * 通过角色ID删除角色和部门关联
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int deleteRoleDeptByRoleId(Long roleId);
+
+ /**
+ * 批量删除角色部门关联信息
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteRoleDept(Long[] ids);
+
+ /**
+ * 查询部门使用数量
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public int selectCountRoleDeptByDeptId(Long deptId);
+
+ /**
+ * 批量新增角色部门信息
+ *
+ * @param roleDeptList 角色部门列表
+ * @return 结果
+ */
+ public int batchRoleDept(List<SysRoleDept> roleDeptList);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java
new file mode 100644
index 0000000..cf2bd8c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java
@@ -0,0 +1,107 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.entity.SysRole;
+
+/**
+ * 角色表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysRoleMapper
+{
+ /**
+ * 根据条件分页查询角色数据
+ *
+ * @param role 角色信息
+ * @return 角色数据集合信息
+ */
+ public List<SysRole> selectRoleList(SysRole role);
+
+ /**
+ * 根据用户ID查询角色
+ *
+ * @param userId 用户ID
+ * @return 角色列表
+ */
+ public List<SysRole> selectRolePermissionByUserId(Long userId);
+
+ /**
+ * 查询所有角色
+ *
+ * @return 角色列表
+ */
+ public List<SysRole> selectRoleAll();
+
+ /**
+ * 根据用户ID获取角色选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中角色ID列表
+ */
+ public List<Long> selectRoleListByUserId(Long userId);
+
+ /**
+ * 通过角色ID查询角色
+ *
+ * @param roleId 角色ID
+ * @return 角色对象信息
+ */
+ public SysRole selectRoleById(Long roleId);
+
+ /**
+ * 根据用户ID查询角色
+ *
+ * @param userName 用户名
+ * @return 角色列表
+ */
+ public List<SysRole> selectRolesByUserName(String userName);
+
+ /**
+ * 校验角色名称是否唯一
+ *
+ * @param roleName 角色名称
+ * @return 角色信息
+ */
+ public SysRole checkRoleNameUnique(String roleName);
+
+ /**
+ * 校验角色权限是否唯一
+ *
+ * @param roleKey 角色权限
+ * @return 角色信息
+ */
+ public SysRole checkRoleKeyUnique(String roleKey);
+
+ /**
+ * 修改角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int updateRole(SysRole role);
+
+ /**
+ * 新增角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int insertRole(SysRole role);
+
+ /**
+ * 通过角色ID删除角色
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int deleteRoleById(Long roleId);
+
+ /**
+ * 批量删除角色信息
+ *
+ * @param roleIds 需要删除的角色ID
+ * @return 结果
+ */
+ public int deleteRoleByIds(Long[] roleIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java
new file mode 100644
index 0000000..6602bee
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java
@@ -0,0 +1,44 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysRoleMenu;
+
+/**
+ * 角色与菜单关联表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysRoleMenuMapper
+{
+ /**
+ * 查询菜单使用数量
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ public int checkMenuExistRole(Long menuId);
+
+ /**
+ * 通过角色ID删除角色和菜单关联
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int deleteRoleMenuByRoleId(Long roleId);
+
+ /**
+ * 批量删除角色菜单关联信息
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteRoleMenu(Long[] ids);
+
+ /**
+ * 批量新增角色菜单信息
+ *
+ * @param roleMenuList 角色菜单列表
+ * @return 结果
+ */
+ public int batchRoleMenu(List<SysRoleMenu> roleMenuList);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
new file mode 100644
index 0000000..76e1c79
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
@@ -0,0 +1,127 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.common.core.domain.entity.SysUser;
+
+/**
+ * 用户表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysUserMapper
+{
+ /**
+ * 根据条件分页查询用户列表
+ *
+ * @param sysUser 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectUserList(SysUser sysUser);
+
+ /**
+ * 根据条件分页查询已配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectAllocatedList(SysUser user);
+
+ /**
+ * 根据条件分页查询未分配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectUnallocatedList(SysUser user);
+
+ /**
+ * 通过用户名查询用户
+ *
+ * @param userName 用户名
+ * @return 用户对象信息
+ */
+ public SysUser selectUserByUserName(String userName);
+
+ /**
+ * 通过用户ID查询用户
+ *
+ * @param userId 用户ID
+ * @return 用户对象信息
+ */
+ public SysUser selectUserById(Long userId);
+
+ /**
+ * 新增用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int insertUser(SysUser user);
+
+ /**
+ * 修改用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int updateUser(SysUser user);
+
+ /**
+ * 修改用户头像
+ *
+ * @param userName 用户名
+ * @param avatar 头像地址
+ * @return 结果
+ */
+ public int updateUserAvatar(@Param("userName") String userName, @Param("avatar") String avatar);
+
+ /**
+ * 重置用户密码
+ *
+ * @param userName 用户名
+ * @param password 密码
+ * @return 结果
+ */
+ public int resetUserPwd(@Param("userName") String userName, @Param("password") String password);
+
+ /**
+ * 通过用户ID删除用户
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int deleteUserById(Long userId);
+
+ /**
+ * 批量删除用户信息
+ *
+ * @param userIds 需要删除的用户ID
+ * @return 结果
+ */
+ public int deleteUserByIds(Long[] userIds);
+
+ /**
+ * 校验用户名称是否唯一
+ *
+ * @param userName 用户名称
+ * @return 结果
+ */
+ public SysUser checkUserNameUnique(String userName);
+
+ /**
+ * 校验手机号码是否唯一
+ *
+ * @param phonenumber 手机号码
+ * @return 结果
+ */
+ public SysUser checkPhoneUnique(String phonenumber);
+
+ /**
+ * 校验email是否唯一
+ *
+ * @param email 用户邮箱
+ * @return 结果
+ */
+ public SysUser checkEmailUnique(String email);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java
new file mode 100644
index 0000000..2a6a720
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java
@@ -0,0 +1,44 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysUserPost;
+
+/**
+ * 用户与岗位关联表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysUserPostMapper
+{
+ /**
+ * 通过用户ID删除用户和岗位关联
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int deleteUserPostByUserId(Long userId);
+
+ /**
+ * 通过岗位ID查询岗位使用数量
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ public int countUserPostById(Long postId);
+
+ /**
+ * 批量删除用户和岗位关联
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteUserPost(Long[] ids);
+
+ /**
+ * 批量新增用户岗位信息
+ *
+ * @param userPostList 用户岗位列表
+ * @return 结果
+ */
+ public int batchUserPost(List<SysUserPost> userPostList);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java
new file mode 100644
index 0000000..3143ec8
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java
@@ -0,0 +1,62 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.system.domain.SysUserRole;
+
+/**
+ * 用户与角色关联表 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysUserRoleMapper
+{
+ /**
+ * 通过用户ID删除用户和角色关联
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int deleteUserRoleByUserId(Long userId);
+
+ /**
+ * 批量删除用户和角色关联
+ *
+ * @param ids 需要删除的数据ID
+ * @return 结果
+ */
+ public int deleteUserRole(Long[] ids);
+
+ /**
+ * 通过角色ID查询角色使用数量
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int countUserRoleByRoleId(Long roleId);
+
+ /**
+ * 批量新增用户角色信息
+ *
+ * @param userRoleList 用户角色列表
+ * @return 结果
+ */
+ public int batchUserRole(List<SysUserRole> userRoleList);
+
+ /**
+ * 删除用户和角色关联信息
+ *
+ * @param userRole 用户和角色关联信息
+ * @return 结果
+ */
+ public int deleteUserRoleInfo(SysUserRole userRole);
+
+ /**
+ * 批量取消授权用户角色
+ *
+ * @param roleId 角色ID
+ * @param userIds 需要删除的用户数据ID
+ * @return 结果
+ */
+ public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java
new file mode 100644
index 0000000..b307776
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java
@@ -0,0 +1,89 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysConfig;
+
+/**
+ * 参数配置 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysConfigService
+{
+ /**
+ * 查询参数配置信息
+ *
+ * @param configId 参数配置ID
+ * @return 参数配置信息
+ */
+ public SysConfig selectConfigById(Long configId);
+
+ /**
+ * 根据键名查询参数配置信息
+ *
+ * @param configKey 参数键名
+ * @return 参数键值
+ */
+ public String selectConfigByKey(String configKey);
+
+ /**
+ * 获取验证码开关
+ *
+ * @return true开启,false关闭
+ */
+ public boolean selectCaptchaEnabled();
+
+ /**
+ * 查询参数配置列表
+ *
+ * @param config 参数配置信息
+ * @return 参数配置集合
+ */
+ public List<SysConfig> selectConfigList(SysConfig config);
+
+ /**
+ * 新增参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ public int insertConfig(SysConfig config);
+
+ /**
+ * 修改参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ public int updateConfig(SysConfig config);
+
+ /**
+ * 批量删除参数信息
+ *
+ * @param configIds 需要删除的参数ID
+ */
+ public void deleteConfigByIds(Long[] configIds);
+
+ /**
+ * 加载参数缓存数据
+ */
+ public void loadingConfigCache();
+
+ /**
+ * 清空参数缓存数据
+ */
+ public void clearConfigCache();
+
+ /**
+ * 重置参数缓存数据
+ */
+ public void resetConfigCache();
+
+ /**
+ * 校验参数键名是否唯一
+ *
+ * @param config 参数信息
+ * @return 结果
+ */
+ public boolean checkConfigKeyUnique(SysConfig config);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java
new file mode 100644
index 0000000..f228208
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java
@@ -0,0 +1,124 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.TreeSelect;
+import com.ruoyi.common.core.domain.entity.SysDept;
+
+/**
+ * 部门管理 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysDeptService
+{
+ /**
+ * 查询部门管理数据
+ *
+ * @param dept 部门信息
+ * @return 部门信息集合
+ */
+ public List<SysDept> selectDeptList(SysDept dept);
+
+ /**
+ * 查询部门树结构信息
+ *
+ * @param dept 部门信息
+ * @return 部门树信息集合
+ */
+ public List<TreeSelect> selectDeptTreeList(SysDept dept);
+
+ /**
+ * 构建前端所需要树结构
+ *
+ * @param depts 部门列表
+ * @return 树结构列表
+ */
+ public List<SysDept> buildDeptTree(List<SysDept> depts);
+
+ /**
+ * 构建前端所需要下拉树结构
+ *
+ * @param depts 部门列表
+ * @return 下拉树结构列表
+ */
+ public List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts);
+
+ /**
+ * 根据角色ID查询部门树信息
+ *
+ * @param roleId 角色ID
+ * @return 选中部门列表
+ */
+ public List<Long> selectDeptListByRoleId(Long roleId);
+
+ /**
+ * 根据部门ID查询信息
+ *
+ * @param deptId 部门ID
+ * @return 部门信息
+ */
+ public SysDept selectDeptById(Long deptId);
+
+ /**
+ * 根据ID查询所有子部门(正常状态)
+ *
+ * @param deptId 部门ID
+ * @return 子部门数
+ */
+ public int selectNormalChildrenDeptById(Long deptId);
+
+ /**
+ * 是否存在部门子节点
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public boolean hasChildByDeptId(Long deptId);
+
+ /**
+ * 查询部门是否存在用户
+ *
+ * @param deptId 部门ID
+ * @return 结果 true 存在 false 不存在
+ */
+ public boolean checkDeptExistUser(Long deptId);
+
+ /**
+ * 校验部门名称是否唯一
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ public boolean checkDeptNameUnique(SysDept dept);
+
+ /**
+ * 校验部门是否有数据权限
+ *
+ * @param deptId 部门id
+ */
+ public void checkDeptDataScope(Long deptId);
+
+ /**
+ * 新增保存部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ public int insertDept(SysDept dept);
+
+ /**
+ * 修改保存部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ public int updateDept(SysDept dept);
+
+ /**
+ * 删除部门管理信息
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ public int deleteDeptById(Long deptId);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java
new file mode 100644
index 0000000..9bc4f13
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java
@@ -0,0 +1,60 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+
+/**
+ * 字典 业务层
+ *
+ * @author ruoyi
+ */
+public interface ISysDictDataService
+{
+ /**
+ * 根据条件分页查询字典数据
+ *
+ * @param dictData 字典数据信息
+ * @return 字典数据集合信息
+ */
+ public List<SysDictData> selectDictDataList(SysDictData dictData);
+
+ /**
+ * 根据字典类型和字典键值查询字典数据信息
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典键值
+ * @return 字典标签
+ */
+ public String selectDictLabel(String dictType, String dictValue);
+
+ /**
+ * 根据字典数据ID查询信息
+ *
+ * @param dictCode 字典数据ID
+ * @return 字典数据
+ */
+ public SysDictData selectDictDataById(Long dictCode);
+
+ /**
+ * 批量删除字典数据信息
+ *
+ * @param dictCodes 需要删除的字典数据ID
+ */
+ public void deleteDictDataByIds(Long[] dictCodes);
+
+ /**
+ * 新增保存字典数据信息
+ *
+ * @param dictData 字典数据信息
+ * @return 结果
+ */
+ public int insertDictData(SysDictData dictData);
+
+ /**
+ * 修改保存字典数据信息
+ *
+ * @param dictData 字典数据信息
+ * @return 结果
+ */
+ public int updateDictData(SysDictData dictData);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java
new file mode 100644
index 0000000..01c1c1d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java
@@ -0,0 +1,98 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.core.domain.entity.SysDictType;
+
+/**
+ * 字典 业务层
+ *
+ * @author ruoyi
+ */
+public interface ISysDictTypeService
+{
+ /**
+ * 根据条件分页查询字典类型
+ *
+ * @param dictType 字典类型信息
+ * @return 字典类型集合信息
+ */
+ public List<SysDictType> selectDictTypeList(SysDictType dictType);
+
+ /**
+ * 根据所有字典类型
+ *
+ * @return 字典类型集合信息
+ */
+ public List<SysDictType> selectDictTypeAll();
+
+ /**
+ * 根据字典类型查询字典数据
+ *
+ * @param dictType 字典类型
+ * @return 字典数据集合信息
+ */
+ public List<SysDictData> selectDictDataByType(String dictType);
+
+ /**
+ * 根据字典类型ID查询信息
+ *
+ * @param dictId 字典类型ID
+ * @return 字典类型
+ */
+ public SysDictType selectDictTypeById(Long dictId);
+
+ /**
+ * 根据字典类型查询信息
+ *
+ * @param dictType 字典类型
+ * @return 字典类型
+ */
+ public SysDictType selectDictTypeByType(String dictType);
+
+ /**
+ * 批量删除字典信息
+ *
+ * @param dictIds 需要删除的字典ID
+ */
+ public void deleteDictTypeByIds(Long[] dictIds);
+
+ /**
+ * 加载字典缓存数据
+ */
+ public void loadingDictCache();
+
+ /**
+ * 清空字典缓存数据
+ */
+ public void clearDictCache();
+
+ /**
+ * 重置字典缓存数据
+ */
+ public void resetDictCache();
+
+ /**
+ * 新增保存字典类型信息
+ *
+ * @param dictType 字典类型信息
+ * @return 结果
+ */
+ public int insertDictType(SysDictType dictType);
+
+ /**
+ * 修改保存字典类型信息
+ *
+ * @param dictType 字典类型信息
+ * @return 结果
+ */
+ public int updateDictType(SysDictType dictType);
+
+ /**
+ * 校验字典类型称是否唯一
+ *
+ * @param dictType 字典类型
+ * @return 结果
+ */
+ public boolean checkDictTypeUnique(SysDictType dictType);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java
new file mode 100644
index 0000000..ce3151d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java
@@ -0,0 +1,40 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysLogininfor;
+
+/**
+ * 系统访问日志情况信息 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysLogininforService
+{
+ /**
+ * 新增系统登录日志
+ *
+ * @param logininfor 访问日志对象
+ */
+ public void insertLogininfor(SysLogininfor logininfor);
+
+ /**
+ * 查询系统登录日志集合
+ *
+ * @param logininfor 访问日志对象
+ * @return 登录记录集合
+ */
+ public List<SysLogininfor> selectLogininforList(SysLogininfor logininfor);
+
+ /**
+ * 批量删除系统登录日志
+ *
+ * @param infoIds 需要删除的登录日志ID
+ * @return 结果
+ */
+ public int deleteLogininforByIds(Long[] infoIds);
+
+ /**
+ * 清空系统登录日志
+ */
+ public void cleanLogininfor();
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java
new file mode 100644
index 0000000..7d60696
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java
@@ -0,0 +1,144 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import java.util.Set;
+import com.ruoyi.common.core.domain.TreeSelect;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.system.domain.vo.RouterVo;
+
+/**
+ * 菜单 业务层
+ *
+ * @author ruoyi
+ */
+public interface ISysMenuService
+{
+ /**
+ * 根据用户查询系统菜单列表
+ *
+ * @param userId 用户ID
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuList(Long userId);
+
+ /**
+ * 根据用户查询系统菜单列表
+ *
+ * @param menu 菜单信息
+ * @param userId 用户ID
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuList(SysMenu menu, Long userId);
+
+ /**
+ * 根据用户ID查询权限
+ *
+ * @param userId 用户ID
+ * @return 权限列表
+ */
+ public Set<String> selectMenuPermsByUserId(Long userId);
+
+ /**
+ * 根据角色ID查询权限
+ *
+ * @param roleId 角色ID
+ * @return 权限列表
+ */
+ public Set<String> selectMenuPermsByRoleId(Long roleId);
+
+ /**
+ * 根据用户ID查询菜单树信息
+ *
+ * @param userId 用户ID
+ * @return 菜单列表
+ */
+ public List<SysMenu> selectMenuTreeByUserId(Long userId);
+
+ /**
+ * 根据角色ID查询菜单树信息
+ *
+ * @param roleId 角色ID
+ * @return 选中菜单列表
+ */
+ public List<Long> selectMenuListByRoleId(Long roleId);
+
+ /**
+ * 构建前端路由所需要的菜单
+ *
+ * @param menus 菜单列表
+ * @return 路由列表
+ */
+ public List<RouterVo> buildMenus(List<SysMenu> menus);
+
+ /**
+ * 构建前端所需要树结构
+ *
+ * @param menus 菜单列表
+ * @return 树结构列表
+ */
+ public List<SysMenu> buildMenuTree(List<SysMenu> menus);
+
+ /**
+ * 构建前端所需要下拉树结构
+ *
+ * @param menus 菜单列表
+ * @return 下拉树结构列表
+ */
+ public List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus);
+
+ /**
+ * 根据菜单ID查询信息
+ *
+ * @param menuId 菜单ID
+ * @return 菜单信息
+ */
+ public SysMenu selectMenuById(Long menuId);
+
+ /**
+ * 是否存在菜单子节点
+ *
+ * @param menuId 菜单ID
+ * @return 结果 true 存在 false 不存在
+ */
+ public boolean hasChildByMenuId(Long menuId);
+
+ /**
+ * 查询菜单是否存在角色
+ *
+ * @param menuId 菜单ID
+ * @return 结果 true 存在 false 不存在
+ */
+ public boolean checkMenuExistRole(Long menuId);
+
+ /**
+ * 新增保存菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public int insertMenu(SysMenu menu);
+
+ /**
+ * 修改保存菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public int updateMenu(SysMenu menu);
+
+ /**
+ * 删除菜单管理信息
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ public int deleteMenuById(Long menuId);
+
+ /**
+ * 校验菜单名称是否唯一
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public boolean checkMenuNameUnique(SysMenu menu);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java
new file mode 100644
index 0000000..47ce1b7
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java
@@ -0,0 +1,60 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysNotice;
+
+/**
+ * 公告 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysNoticeService
+{
+ /**
+ * 查询公告信息
+ *
+ * @param noticeId 公告ID
+ * @return 公告信息
+ */
+ public SysNotice selectNoticeById(Long noticeId);
+
+ /**
+ * 查询公告列表
+ *
+ * @param notice 公告信息
+ * @return 公告集合
+ */
+ public List<SysNotice> selectNoticeList(SysNotice notice);
+
+ /**
+ * 新增公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ public int insertNotice(SysNotice notice);
+
+ /**
+ * 修改公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ public int updateNotice(SysNotice notice);
+
+ /**
+ * 删除公告信息
+ *
+ * @param noticeId 公告ID
+ * @return 结果
+ */
+ public int deleteNoticeById(Long noticeId);
+
+ /**
+ * 批量删除公告信息
+ *
+ * @param noticeIds 需要删除的公告ID
+ * @return 结果
+ */
+ public int deleteNoticeByIds(Long[] noticeIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java
new file mode 100644
index 0000000..4fd8e5a
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java
@@ -0,0 +1,48 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysOperLog;
+
+/**
+ * 操作日志 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysOperLogService
+{
+ /**
+ * 新增操作日志
+ *
+ * @param operLog 操作日志对象
+ */
+ public void insertOperlog(SysOperLog operLog);
+
+ /**
+ * 查询系统操作日志集合
+ *
+ * @param operLog 操作日志对象
+ * @return 操作日志集合
+ */
+ public List<SysOperLog> selectOperLogList(SysOperLog operLog);
+
+ /**
+ * 批量删除系统操作日志
+ *
+ * @param operIds 需要删除的操作日志ID
+ * @return 结果
+ */
+ public int deleteOperLogByIds(Long[] operIds);
+
+ /**
+ * 查询操作日志详细
+ *
+ * @param operId 操作ID
+ * @return 操作日志对象
+ */
+ public SysOperLog selectOperLogById(Long operId);
+
+ /**
+ * 清空操作日志
+ */
+ public void cleanOperLog();
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java
new file mode 100644
index 0000000..84779bf
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java
@@ -0,0 +1,99 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysPost;
+
+/**
+ * 岗位信息 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysPostService
+{
+ /**
+ * 查询岗位信息集合
+ *
+ * @param post 岗位信息
+ * @return 岗位列表
+ */
+ public List<SysPost> selectPostList(SysPost post);
+
+ /**
+ * 查询所有岗位
+ *
+ * @return 岗位列表
+ */
+ public List<SysPost> selectPostAll();
+
+ /**
+ * 通过岗位ID查询岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 角色对象信息
+ */
+ public SysPost selectPostById(Long postId);
+
+ /**
+ * 根据用户ID获取岗位选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中岗位ID列表
+ */
+ public List<Long> selectPostListByUserId(Long userId);
+
+ /**
+ * 校验岗位名称
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public boolean checkPostNameUnique(SysPost post);
+
+ /**
+ * 校验岗位编码
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public boolean checkPostCodeUnique(SysPost post);
+
+ /**
+ * 通过岗位ID查询岗位使用数量
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ public int countUserPostById(Long postId);
+
+ /**
+ * 删除岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ public int deletePostById(Long postId);
+
+ /**
+ * 批量删除岗位信息
+ *
+ * @param postIds 需要删除的岗位ID
+ * @return 结果
+ */
+ public int deletePostByIds(Long[] postIds);
+
+ /**
+ * 新增保存岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public int insertPost(SysPost post);
+
+ /**
+ * 修改保存岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ public int updatePost(SysPost post);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java
new file mode 100644
index 0000000..9185cce
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java
@@ -0,0 +1,173 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import java.util.Set;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.system.domain.SysUserRole;
+
+/**
+ * 角色业务层
+ *
+ * @author ruoyi
+ */
+public interface ISysRoleService
+{
+ /**
+ * 根据条件分页查询角色数据
+ *
+ * @param role 角色信息
+ * @return 角色数据集合信息
+ */
+ public List<SysRole> selectRoleList(SysRole role);
+
+ /**
+ * 根据用户ID查询角色列表
+ *
+ * @param userId 用户ID
+ * @return 角色列表
+ */
+ public List<SysRole> selectRolesByUserId(Long userId);
+
+ /**
+ * 根据用户ID查询角色权限
+ *
+ * @param userId 用户ID
+ * @return 权限列表
+ */
+ public Set<String> selectRolePermissionByUserId(Long userId);
+
+ /**
+ * 查询所有角色
+ *
+ * @return 角色列表
+ */
+ public List<SysRole> selectRoleAll();
+
+ /**
+ * 根据用户ID获取角色选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中角色ID列表
+ */
+ public List<Long> selectRoleListByUserId(Long userId);
+
+ /**
+ * 通过角色ID查询角色
+ *
+ * @param roleId 角色ID
+ * @return 角色对象信息
+ */
+ public SysRole selectRoleById(Long roleId);
+
+ /**
+ * 校验角色名称是否唯一
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public boolean checkRoleNameUnique(SysRole role);
+
+ /**
+ * 校验角色权限是否唯一
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public boolean checkRoleKeyUnique(SysRole role);
+
+ /**
+ * 校验角色是否允许操作
+ *
+ * @param role 角色信息
+ */
+ public void checkRoleAllowed(SysRole role);
+
+ /**
+ * 校验角色是否有数据权限
+ *
+ * @param roleIds 角色id
+ */
+ public void checkRoleDataScope(Long... roleIds);
+
+ /**
+ * 通过角色ID查询角色使用数量
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int countUserRoleByRoleId(Long roleId);
+
+ /**
+ * 新增保存角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int insertRole(SysRole role);
+
+ /**
+ * 修改保存角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int updateRole(SysRole role);
+
+ /**
+ * 修改角色状态
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int updateRoleStatus(SysRole role);
+
+ /**
+ * 修改数据权限信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ public int authDataScope(SysRole role);
+
+ /**
+ * 通过角色ID删除角色
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ public int deleteRoleById(Long roleId);
+
+ /**
+ * 批量删除角色信息
+ *
+ * @param roleIds 需要删除的角色ID
+ * @return 结果
+ */
+ public int deleteRoleByIds(Long[] roleIds);
+
+ /**
+ * 取消授权用户角色
+ *
+ * @param userRole 用户和角色关联信息
+ * @return 结果
+ */
+ public int deleteAuthUser(SysUserRole userRole);
+
+ /**
+ * 批量取消授权用户角色
+ *
+ * @param roleId 角色ID
+ * @param userIds 需要取消授权的用户数据ID
+ * @return 结果
+ */
+ public int deleteAuthUsers(Long roleId, Long[] userIds);
+
+ /**
+ * 批量选择授权用户角色
+ *
+ * @param roleId 角色ID
+ * @param userIds 需要删除的用户数据ID
+ * @return 结果
+ */
+ public int insertAuthUsers(Long roleId, Long[] userIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java
new file mode 100644
index 0000000..8eb5448
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java
@@ -0,0 +1,48 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.system.domain.SysUserOnline;
+
+/**
+ * 在线用户 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysUserOnlineService
+{
+ /**
+ * 通过登录地址查询信息
+ *
+ * @param ipaddr 登录地址
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user);
+
+ /**
+ * 通过用户名称查询信息
+ *
+ * @param userName 用户名称
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ public SysUserOnline selectOnlineByUserName(String userName, LoginUser user);
+
+ /**
+ * 通过登录地址/用户名称查询信息
+ *
+ * @param ipaddr 登录地址
+ * @param userName 用户名称
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user);
+
+ /**
+ * 设置在线用户信息
+ *
+ * @param user 用户信息
+ * @return 在线用户
+ */
+ public SysUserOnline loginUserToUserOnline(LoginUser user);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
new file mode 100644
index 0000000..10bc2ab
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
@@ -0,0 +1,206 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.common.core.domain.entity.SysUser;
+
+/**
+ * 用户 业务层
+ *
+ * @author ruoyi
+ */
+public interface ISysUserService
+{
+ /**
+ * 根据条件分页查询用户列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectUserList(SysUser user);
+
+ /**
+ * 根据条件分页查询已分配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectAllocatedList(SysUser user);
+
+ /**
+ * 根据条件分页查询未分配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ public List<SysUser> selectUnallocatedList(SysUser user);
+
+ /**
+ * 通过用户名查询用户
+ *
+ * @param userName 用户名
+ * @return 用户对象信息
+ */
+ public SysUser selectUserByUserName(String userName);
+
+ /**
+ * 通过用户ID查询用户
+ *
+ * @param userId 用户ID
+ * @return 用户对象信息
+ */
+ public SysUser selectUserById(Long userId);
+
+ /**
+ * 根据用户ID查询用户所属角色组
+ *
+ * @param userName 用户名
+ * @return 结果
+ */
+ public String selectUserRoleGroup(String userName);
+
+ /**
+ * 根据用户ID查询用户所属岗位组
+ *
+ * @param userName 用户名
+ * @return 结果
+ */
+ public String selectUserPostGroup(String userName);
+
+ /**
+ * 校验用户名称是否唯一
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public boolean checkUserNameUnique(SysUser user);
+
+ /**
+ * 校验手机号码是否唯一
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public boolean checkPhoneUnique(SysUser user);
+
+ /**
+ * 校验email是否唯一
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public boolean checkEmailUnique(SysUser user);
+
+ /**
+ * 校验用户是否允许操作
+ *
+ * @param user 用户信息
+ */
+ public void checkUserAllowed(SysUser user);
+
+ /**
+ * 校验用户是否有数据权限
+ *
+ * @param userId 用户id
+ */
+ public void checkUserDataScope(Long userId);
+
+ /**
+ * 新增用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int insertUser(SysUser user);
+
+ /**
+ * 注册用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public boolean registerUser(SysUser user);
+
+ /**
+ * 修改用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int updateUser(SysUser user);
+
+ /**
+ * 用户授权角色
+ *
+ * @param userId 用户ID
+ * @param roleIds 角色组
+ */
+ public void insertUserAuth(Long userId, Long[] roleIds);
+
+ /**
+ * 修改用户状态
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int updateUserStatus(SysUser user);
+
+ /**
+ * 修改用户基本信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int updateUserProfile(SysUser user);
+
+ /**
+ * 修改用户头像
+ *
+ * @param userName 用户名
+ * @param avatar 头像地址
+ * @return 结果
+ */
+ public boolean updateUserAvatar(String userName, String avatar);
+
+ /**
+ * 重置用户密码
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ public int resetPwd(SysUser user);
+
+ /**
+ * 重置用户密码
+ *
+ * @param userName 用户名
+ * @param password 密码
+ * @return 结果
+ */
+ public int resetUserPwd(String userName, String password);
+
+ /**
+ * 通过用户ID删除用户
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public int deleteUserById(Long userId);
+
+ /**
+ * 批量删除用户信息
+ *
+ * @param userIds 需要删除的用户ID
+ * @return 结果
+ */
+ public int deleteUserByIds(Long[] userIds);
+
+ /**
+ * 导入用户数据
+ *
+ * @param userList 用户数据列表
+ * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+ * @param operName 操作用户
+ * @return 结果
+ */
+ public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java
new file mode 100644
index 0000000..83750e7
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java
@@ -0,0 +1,232 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Collection;
+import java.util.List;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.annotation.DataSource;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.enums.DataSourceType;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysConfig;
+import com.ruoyi.system.mapper.SysConfigMapper;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 参数配置 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysConfigServiceImpl implements ISysConfigService
+{
+ @Autowired
+ private SysConfigMapper configMapper;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ /**
+ * 项目启动时,初始化参数到缓存
+ */
+ @PostConstruct
+ public void init()
+ {
+ loadingConfigCache();
+ }
+
+ /**
+ * 查询参数配置信息
+ *
+ * @param configId 参数配置ID
+ * @return 参数配置信息
+ */
+ @Override
+ @DataSource(DataSourceType.MASTER)
+ public SysConfig selectConfigById(Long configId)
+ {
+ SysConfig config = new SysConfig();
+ config.setConfigId(configId);
+ return configMapper.selectConfig(config);
+ }
+
+ /**
+ * 根据键名查询参数配置信息
+ *
+ * @param configKey 参数key
+ * @return 参数键值
+ */
+ @Override
+ public String selectConfigByKey(String configKey)
+ {
+ String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
+ if (StringUtils.isNotEmpty(configValue))
+ {
+ return configValue;
+ }
+ SysConfig config = new SysConfig();
+ config.setConfigKey(configKey);
+ SysConfig retConfig = configMapper.selectConfig(config);
+ if (StringUtils.isNotNull(retConfig))
+ {
+ redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
+ return retConfig.getConfigValue();
+ }
+ return StringUtils.EMPTY;
+ }
+
+ /**
+ * 获取验证码开关
+ *
+ * @return true开启,false关闭
+ */
+ @Override
+ public boolean selectCaptchaEnabled()
+ {
+ String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled");
+ if (StringUtils.isEmpty(captchaEnabled))
+ {
+ return true;
+ }
+ return Convert.toBool(captchaEnabled);
+ }
+
+ /**
+ * 查询参数配置列表
+ *
+ * @param config 参数配置信息
+ * @return 参数配置集合
+ */
+ @Override
+ public List<SysConfig> selectConfigList(SysConfig config)
+ {
+ return configMapper.selectConfigList(config);
+ }
+
+ /**
+ * 新增参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ @Override
+ public int insertConfig(SysConfig config)
+ {
+ int row = configMapper.insertConfig(config);
+ if (row > 0)
+ {
+ redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+ }
+ return row;
+ }
+
+ /**
+ * 修改参数配置
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ @Override
+ public int updateConfig(SysConfig config)
+ {
+ SysConfig temp = configMapper.selectConfigById(config.getConfigId());
+ if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey()))
+ {
+ redisCache.deleteObject(getCacheKey(temp.getConfigKey()));
+ }
+
+ int row = configMapper.updateConfig(config);
+ if (row > 0)
+ {
+ redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+ }
+ return row;
+ }
+
+ /**
+ * 批量删除参数信息
+ *
+ * @param configIds 需要删除的参数ID
+ */
+ @Override
+ public void deleteConfigByIds(Long[] configIds)
+ {
+ for (Long configId : configIds)
+ {
+ SysConfig config = selectConfigById(configId);
+ if (StringUtils.equals(UserConstants.YES, config.getConfigType()))
+ {
+ throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey()));
+ }
+ configMapper.deleteConfigById(configId);
+ redisCache.deleteObject(getCacheKey(config.getConfigKey()));
+ }
+ }
+
+ /**
+ * 加载参数缓存数据
+ */
+ @Override
+ public void loadingConfigCache()
+ {
+ List<SysConfig> configsList = configMapper.selectConfigList(new SysConfig());
+ for (SysConfig config : configsList)
+ {
+ redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+ }
+ }
+
+ /**
+ * 清空参数缓存数据
+ */
+ @Override
+ public void clearConfigCache()
+ {
+ Collection<String> keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*");
+ redisCache.deleteObject(keys);
+ }
+
+ /**
+ * 重置参数缓存数据
+ */
+ @Override
+ public void resetConfigCache()
+ {
+ clearConfigCache();
+ loadingConfigCache();
+ }
+
+ /**
+ * 校验参数键名是否唯一
+ *
+ * @param config 参数配置信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkConfigKeyUnique(SysConfig config)
+ {
+ Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId();
+ SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey());
+ if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 设置cache key
+ *
+ * @param configKey 参数键
+ * @return 缓存键key
+ */
+ private String getCacheKey(String configKey)
+ {
+ return CacheConstants.SYS_CONFIG_KEY + configKey;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java
new file mode 100644
index 0000000..54b605d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java
@@ -0,0 +1,338 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.annotation.DataScope;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.TreeSelect;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.mapper.SysDeptMapper;
+import com.ruoyi.system.mapper.SysRoleMapper;
+import com.ruoyi.system.service.ISysDeptService;
+
+/**
+ * 部门管理 服务实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysDeptServiceImpl implements ISysDeptService
+{
+ @Autowired
+ private SysDeptMapper deptMapper;
+
+ @Autowired
+ private SysRoleMapper roleMapper;
+
+ /**
+ * 查询部门管理数据
+ *
+ * @param dept 部门信息
+ * @return 部门信息集合
+ */
+ @Override
+ @DataScope(deptAlias = "d")
+ public List<SysDept> selectDeptList(SysDept dept)
+ {
+ return deptMapper.selectDeptList(dept);
+ }
+
+ /**
+ * 查询部门树结构信息
+ *
+ * @param dept 部门信息
+ * @return 部门树信息集合
+ */
+ @Override
+ public List<TreeSelect> selectDeptTreeList(SysDept dept)
+ {
+ List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);
+ return buildDeptTreeSelect(depts);
+ }
+
+ /**
+ * 构建前端所需要树结构
+ *
+ * @param depts 部门列表
+ * @return 树结构列表
+ */
+ @Override
+ public List<SysDept> buildDeptTree(List<SysDept> depts)
+ {
+ List<SysDept> returnList = new ArrayList<SysDept>();
+ List<Long> tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList());
+ for (SysDept dept : depts)
+ {
+ // 如果是顶级节点, 遍历该父节点的所有子节点
+ if (!tempList.contains(dept.getParentId()))
+ {
+ recursionFn(depts, dept);
+ returnList.add(dept);
+ }
+ }
+ if (returnList.isEmpty())
+ {
+ returnList = depts;
+ }
+ return returnList;
+ }
+
+ /**
+ * 构建前端所需要下拉树结构
+ *
+ * @param depts 部门列表
+ * @return 下拉树结构列表
+ */
+ @Override
+ public List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts)
+ {
+ List<SysDept> deptTrees = buildDeptTree(depts);
+ return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
+ }
+
+ /**
+ * 根据角色ID查询部门树信息
+ *
+ * @param roleId 角色ID
+ * @return 选中部门列表
+ */
+ @Override
+ public List<Long> selectDeptListByRoleId(Long roleId)
+ {
+ SysRole role = roleMapper.selectRoleById(roleId);
+ return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly());
+ }
+
+ /**
+ * 根据部门ID查询信息
+ *
+ * @param deptId 部门ID
+ * @return 部门信息
+ */
+ @Override
+ public SysDept selectDeptById(Long deptId)
+ {
+ return deptMapper.selectDeptById(deptId);
+ }
+
+ /**
+ * 根据ID查询所有子部门(正常状态)
+ *
+ * @param deptId 部门ID
+ * @return 子部门数
+ */
+ @Override
+ public int selectNormalChildrenDeptById(Long deptId)
+ {
+ return deptMapper.selectNormalChildrenDeptById(deptId);
+ }
+
+ /**
+ * 是否存在子节点
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ @Override
+ public boolean hasChildByDeptId(Long deptId)
+ {
+ int result = deptMapper.hasChildByDeptId(deptId);
+ return result > 0;
+ }
+
+ /**
+ * 查询部门是否存在用户
+ *
+ * @param deptId 部门ID
+ * @return 结果 true 存在 false 不存在
+ */
+ @Override
+ public boolean checkDeptExistUser(Long deptId)
+ {
+ int result = deptMapper.checkDeptExistUser(deptId);
+ return result > 0;
+ }
+
+ /**
+ * 校验部门名称是否唯一
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkDeptNameUnique(SysDept dept)
+ {
+ Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId();
+ SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId());
+ if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验部门是否有数据权限
+ *
+ * @param deptId 部门id
+ */
+ @Override
+ public void checkDeptDataScope(Long deptId)
+ {
+ if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId))
+ {
+ SysDept dept = new SysDept();
+ dept.setDeptId(deptId);
+ List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);
+ if (StringUtils.isEmpty(depts))
+ {
+ throw new ServiceException("没有权限访问部门数据!");
+ }
+ }
+ }
+
+ /**
+ * 新增保存部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ @Override
+ public int insertDept(SysDept dept)
+ {
+ SysDept info = deptMapper.selectDeptById(dept.getParentId());
+ // 如果父节点不为正常状态,则不允许新增子节点
+ if (!UserConstants.DEPT_NORMAL.equals(info.getStatus()))
+ {
+ throw new ServiceException("部门停用,不允许新增");
+ }
+ dept.setAncestors(info.getAncestors() + "," + dept.getParentId());
+ return deptMapper.insertDept(dept);
+ }
+
+ /**
+ * 修改保存部门信息
+ *
+ * @param dept 部门信息
+ * @return 结果
+ */
+ @Override
+ public int updateDept(SysDept dept)
+ {
+ SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId());
+ SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId());
+ if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept))
+ {
+ String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId();
+ String oldAncestors = oldDept.getAncestors();
+ dept.setAncestors(newAncestors);
+ updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
+ }
+ int result = deptMapper.updateDept(dept);
+ if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors())
+ && !StringUtils.equals("0", dept.getAncestors()))
+ {
+ // 如果该部门是启用状态,则启用该部门的所有上级部门
+ updateParentDeptStatusNormal(dept);
+ }
+ return result;
+ }
+
+ /**
+ * 修改该部门的父级部门状态
+ *
+ * @param dept 当前部门
+ */
+ private void updateParentDeptStatusNormal(SysDept dept)
+ {
+ String ancestors = dept.getAncestors();
+ Long[] deptIds = Convert.toLongArray(ancestors);
+ deptMapper.updateDeptStatusNormal(deptIds);
+ }
+
+ /**
+ * 修改子元素关系
+ *
+ * @param deptId 被修改的部门ID
+ * @param newAncestors 新的父ID集合
+ * @param oldAncestors 旧的父ID集合
+ */
+ public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors)
+ {
+ List<SysDept> children = deptMapper.selectChildrenDeptById(deptId);
+ for (SysDept child : children)
+ {
+ child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));
+ }
+ if (children.size() > 0)
+ {
+ deptMapper.updateDeptChildren(children);
+ }
+ }
+
+ /**
+ * 删除部门管理信息
+ *
+ * @param deptId 部门ID
+ * @return 结果
+ */
+ @Override
+ public int deleteDeptById(Long deptId)
+ {
+ return deptMapper.deleteDeptById(deptId);
+ }
+
+ /**
+ * 递归列表
+ */
+ private void recursionFn(List<SysDept> list, SysDept t)
+ {
+ // 得到子节点列表
+ List<SysDept> childList = getChildList(list, t);
+ t.setChildren(childList);
+ for (SysDept tChild : childList)
+ {
+ if (hasChild(list, tChild))
+ {
+ recursionFn(list, tChild);
+ }
+ }
+ }
+
+ /**
+ * 得到子节点列表
+ */
+ private List<SysDept> getChildList(List<SysDept> list, SysDept t)
+ {
+ List<SysDept> tlist = new ArrayList<SysDept>();
+ Iterator<SysDept> it = list.iterator();
+ while (it.hasNext())
+ {
+ SysDept n = (SysDept) it.next();
+ if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue())
+ {
+ tlist.add(n);
+ }
+ }
+ return tlist;
+ }
+
+ /**
+ * 判断是否有子节点
+ */
+ private boolean hasChild(List<SysDept> list, SysDept t)
+ {
+ return getChildList(list, t).size() > 0;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java
new file mode 100644
index 0000000..fced569
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java
@@ -0,0 +1,111 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.system.mapper.SysDictDataMapper;
+import com.ruoyi.system.service.ISysDictDataService;
+
+/**
+ * 字典 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysDictDataServiceImpl implements ISysDictDataService
+{
+ @Autowired
+ private SysDictDataMapper dictDataMapper;
+
+ /**
+ * 根据条件分页查询字典数据
+ *
+ * @param dictData 字典数据信息
+ * @return 字典数据集合信息
+ */
+ @Override
+ public List<SysDictData> selectDictDataList(SysDictData dictData)
+ {
+ return dictDataMapper.selectDictDataList(dictData);
+ }
+
+ /**
+ * 根据字典类型和字典键值查询字典数据信息
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典键值
+ * @return 字典标签
+ */
+ @Override
+ public String selectDictLabel(String dictType, String dictValue)
+ {
+ return dictDataMapper.selectDictLabel(dictType, dictValue);
+ }
+
+ /**
+ * 根据字典数据ID查询信息
+ *
+ * @param dictCode 字典数据ID
+ * @return 字典数据
+ */
+ @Override
+ public SysDictData selectDictDataById(Long dictCode)
+ {
+ return dictDataMapper.selectDictDataById(dictCode);
+ }
+
+ /**
+ * 批量删除字典数据信息
+ *
+ * @param dictCodes 需要删除的字典数据ID
+ */
+ @Override
+ public void deleteDictDataByIds(Long[] dictCodes)
+ {
+ for (Long dictCode : dictCodes)
+ {
+ SysDictData data = selectDictDataById(dictCode);
+ dictDataMapper.deleteDictDataById(dictCode);
+ List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+ DictUtils.setDictCache(data.getDictType(), dictDatas);
+ }
+ }
+
+ /**
+ * 新增保存字典数据信息
+ *
+ * @param data 字典数据信息
+ * @return 结果
+ */
+ @Override
+ public int insertDictData(SysDictData data)
+ {
+ int row = dictDataMapper.insertDictData(data);
+ if (row > 0)
+ {
+ List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+ DictUtils.setDictCache(data.getDictType(), dictDatas);
+ }
+ return row;
+ }
+
+ /**
+ * 修改保存字典数据信息
+ *
+ * @param data 字典数据信息
+ * @return 结果
+ */
+ @Override
+ public int updateDictData(SysDictData data)
+ {
+ int row = dictDataMapper.updateDictData(data);
+ if (row > 0)
+ {
+ List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+ DictUtils.setDictCache(data.getDictType(), dictDatas);
+ }
+ return row;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
new file mode 100644
index 0000000..37928c4
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
@@ -0,0 +1,223 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.core.domain.entity.SysDictType;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.mapper.SysDictDataMapper;
+import com.ruoyi.system.mapper.SysDictTypeMapper;
+import com.ruoyi.system.service.ISysDictTypeService;
+
+/**
+ * 字典 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysDictTypeServiceImpl implements ISysDictTypeService
+{
+ @Autowired
+ private SysDictTypeMapper dictTypeMapper;
+
+ @Autowired
+ private SysDictDataMapper dictDataMapper;
+
+ /**
+ * 项目启动时,初始化字典到缓存
+ */
+ @PostConstruct
+ public void init()
+ {
+ loadingDictCache();
+ }
+
+ /**
+ * 根据条件分页查询字典类型
+ *
+ * @param dictType 字典类型信息
+ * @return 字典类型集合信息
+ */
+ @Override
+ public List<SysDictType> selectDictTypeList(SysDictType dictType)
+ {
+ return dictTypeMapper.selectDictTypeList(dictType);
+ }
+
+ /**
+ * 根据所有字典类型
+ *
+ * @return 字典类型集合信息
+ */
+ @Override
+ public List<SysDictType> selectDictTypeAll()
+ {
+ return dictTypeMapper.selectDictTypeAll();
+ }
+
+ /**
+ * 根据字典类型查询字典数据
+ *
+ * @param dictType 字典类型
+ * @return 字典数据集合信息
+ */
+ @Override
+ public List<SysDictData> selectDictDataByType(String dictType)
+ {
+ List<SysDictData> dictDatas = DictUtils.getDictCache(dictType);
+ if (StringUtils.isNotEmpty(dictDatas))
+ {
+ return dictDatas;
+ }
+ dictDatas = dictDataMapper.selectDictDataByType(dictType);
+ if (StringUtils.isNotEmpty(dictDatas))
+ {
+ DictUtils.setDictCache(dictType, dictDatas);
+ return dictDatas;
+ }
+ return null;
+ }
+
+ /**
+ * 根据字典类型ID查询信息
+ *
+ * @param dictId 字典类型ID
+ * @return 字典类型
+ */
+ @Override
+ public SysDictType selectDictTypeById(Long dictId)
+ {
+ return dictTypeMapper.selectDictTypeById(dictId);
+ }
+
+ /**
+ * 根据字典类型查询信息
+ *
+ * @param dictType 字典类型
+ * @return 字典类型
+ */
+ @Override
+ public SysDictType selectDictTypeByType(String dictType)
+ {
+ return dictTypeMapper.selectDictTypeByType(dictType);
+ }
+
+ /**
+ * 批量删除字典类型信息
+ *
+ * @param dictIds 需要删除的字典ID
+ */
+ @Override
+ public void deleteDictTypeByIds(Long[] dictIds)
+ {
+ for (Long dictId : dictIds)
+ {
+ SysDictType dictType = selectDictTypeById(dictId);
+ if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0)
+ {
+ throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName()));
+ }
+ dictTypeMapper.deleteDictTypeById(dictId);
+ DictUtils.removeDictCache(dictType.getDictType());
+ }
+ }
+
+ /**
+ * 加载字典缓存数据
+ */
+ @Override
+ public void loadingDictCache()
+ {
+ SysDictData dictData = new SysDictData();
+ dictData.setStatus("0");
+ Map<String, List<SysDictData>> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType));
+ for (Map.Entry<String, List<SysDictData>> entry : dictDataMap.entrySet())
+ {
+ DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList()));
+ }
+ }
+
+ /**
+ * 清空字典缓存数据
+ */
+ @Override
+ public void clearDictCache()
+ {
+ DictUtils.clearDictCache();
+ }
+
+ /**
+ * 重置字典缓存数据
+ */
+ @Override
+ public void resetDictCache()
+ {
+ clearDictCache();
+ loadingDictCache();
+ }
+
+ /**
+ * 新增保存字典类型信息
+ *
+ * @param dict 字典类型信息
+ * @return 结果
+ */
+ @Override
+ public int insertDictType(SysDictType dict)
+ {
+ int row = dictTypeMapper.insertDictType(dict);
+ if (row > 0)
+ {
+ DictUtils.setDictCache(dict.getDictType(), null);
+ }
+ return row;
+ }
+
+ /**
+ * 修改保存字典类型信息
+ *
+ * @param dict 字典类型信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int updateDictType(SysDictType dict)
+ {
+ SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId());
+ dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType());
+ int row = dictTypeMapper.updateDictType(dict);
+ if (row > 0)
+ {
+ List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType());
+ DictUtils.setDictCache(dict.getDictType(), dictDatas);
+ }
+ return row;
+ }
+
+ /**
+ * 校验字典类型称是否唯一
+ *
+ * @param dict 字典类型
+ * @return 结果
+ */
+ @Override
+ public boolean checkDictTypeUnique(SysDictType dict)
+ {
+ Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId();
+ SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType());
+ if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java
new file mode 100644
index 0000000..216aecb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java
@@ -0,0 +1,65 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.domain.SysLogininfor;
+import com.ruoyi.system.mapper.SysLogininforMapper;
+import com.ruoyi.system.service.ISysLogininforService;
+
+/**
+ * 系统访问日志情况信息 服务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysLogininforServiceImpl implements ISysLogininforService
+{
+
+ @Autowired
+ private SysLogininforMapper logininforMapper;
+
+ /**
+ * 新增系统登录日志
+ *
+ * @param logininfor 访问日志对象
+ */
+ @Override
+ public void insertLogininfor(SysLogininfor logininfor)
+ {
+ logininforMapper.insertLogininfor(logininfor);
+ }
+
+ /**
+ * 查询系统登录日志集合
+ *
+ * @param logininfor 访问日志对象
+ * @return 登录记录集合
+ */
+ @Override
+ public List<SysLogininfor> selectLogininforList(SysLogininfor logininfor)
+ {
+ return logininforMapper.selectLogininforList(logininfor);
+ }
+
+ /**
+ * 批量删除系统登录日志
+ *
+ * @param infoIds 需要删除的登录日志ID
+ * @return 结果
+ */
+ @Override
+ public int deleteLogininforByIds(Long[] infoIds)
+ {
+ return logininforMapper.deleteLogininforByIds(infoIds);
+ }
+
+ /**
+ * 清空系统登录日志
+ */
+ @Override
+ public void cleanLogininfor()
+ {
+ logininforMapper.cleanLogininfor();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
new file mode 100644
index 0000000..78a7830
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
@@ -0,0 +1,543 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.TreeSelect;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.vo.MetaVo;
+import com.ruoyi.system.domain.vo.RouterVo;
+import com.ruoyi.system.mapper.SysMenuMapper;
+import com.ruoyi.system.mapper.SysRoleMapper;
+import com.ruoyi.system.mapper.SysRoleMenuMapper;
+import com.ruoyi.system.service.ISysMenuService;
+
+/**
+ * 菜单 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysMenuServiceImpl implements ISysMenuService
+{
+ public static final String PREMISSION_STRING = "perms[\"{0}\"]";
+
+ @Autowired
+ private SysMenuMapper menuMapper;
+
+ @Autowired
+ private SysRoleMapper roleMapper;
+
+ @Autowired
+ private SysRoleMenuMapper roleMenuMapper;
+
+ /**
+ * 根据用户查询系统菜单列表
+ *
+ * @param userId 用户ID
+ * @return 菜单列表
+ */
+ @Override
+ public List<SysMenu> selectMenuList(Long userId)
+ {
+ return selectMenuList(new SysMenu(), userId);
+ }
+
+ /**
+ * 查询系统菜单列表
+ *
+ * @param menu 菜单信息
+ * @return 菜单列表
+ */
+ @Override
+ public List<SysMenu> selectMenuList(SysMenu menu, Long userId)
+ {
+ List<SysMenu> menuList = null;
+ // 管理员显示所有菜单信息
+ if (SysUser.isAdmin(userId))
+ {
+ menuList = menuMapper.selectMenuList(menu);
+ }
+ else
+ {
+ menu.getParams().put("userId", userId);
+ menuList = menuMapper.selectMenuListByUserId(menu);
+ }
+ return menuList;
+ }
+
+ /**
+ * 根据用户ID查询权限
+ *
+ * @param userId 用户ID
+ * @return 权限列表
+ */
+ @Override
+ public Set<String> selectMenuPermsByUserId(Long userId)
+ {
+ List<String> perms = menuMapper.selectMenuPermsByUserId(userId);
+ Set<String> permsSet = new HashSet<>();
+ for (String perm : perms)
+ {
+ if (StringUtils.isNotEmpty(perm))
+ {
+ permsSet.addAll(Arrays.asList(perm.trim().split(",")));
+ }
+ }
+ return permsSet;
+ }
+
+ /**
+ * 根据角色ID查询权限
+ *
+ * @param roleId 角色ID
+ * @return 权限列表
+ */
+ @Override
+ public Set<String> selectMenuPermsByRoleId(Long roleId)
+ {
+ List<String> perms = menuMapper.selectMenuPermsByRoleId(roleId);
+ Set<String> permsSet = new HashSet<>();
+ for (String perm : perms)
+ {
+ if (StringUtils.isNotEmpty(perm))
+ {
+ permsSet.addAll(Arrays.asList(perm.trim().split(",")));
+ }
+ }
+ return permsSet;
+ }
+
+ /**
+ * 根据用户ID查询菜单
+ *
+ * @param userId 用户名称
+ * @return 菜单列表
+ */
+ @Override
+ public List<SysMenu> selectMenuTreeByUserId(Long userId)
+ {
+ List<SysMenu> menus = null;
+ if (SecurityUtils.isAdmin(userId))
+ {
+ menus = menuMapper.selectMenuTreeAll();
+ }
+ else
+ {
+ menus = menuMapper.selectMenuTreeByUserId(userId);
+ }
+ return getChildPerms(menus, 0);
+ }
+
+ /**
+ * 根据角色ID查询菜单树信息
+ *
+ * @param roleId 角色ID
+ * @return 选中菜单列表
+ */
+ @Override
+ public List<Long> selectMenuListByRoleId(Long roleId)
+ {
+ SysRole role = roleMapper.selectRoleById(roleId);
+ return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly());
+ }
+
+ /**
+ * 构建前端路由所需要的菜单
+ *
+ * @param menus 菜单列表
+ * @return 路由列表
+ */
+ @Override
+ public List<RouterVo> buildMenus(List<SysMenu> menus)
+ {
+ List<RouterVo> routers = new LinkedList<RouterVo>();
+ for (SysMenu menu : menus)
+ {
+ RouterVo router = new RouterVo();
+ router.setHidden("1".equals(menu.getVisible()));
+ router.setName(getRouteName(menu));
+ router.setPath(getRouterPath(menu));
+ router.setComponent(getComponent(menu));
+ router.setQuery(menu.getQuery());
+ router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+ List<SysMenu> cMenus = menu.getChildren();
+ if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType()))
+ {
+ router.setAlwaysShow(true);
+ router.setRedirect("noRedirect");
+ router.setChildren(buildMenus(cMenus));
+ }
+ else if (isMenuFrame(menu))
+ {
+ router.setMeta(null);
+ List<RouterVo> childrenList = new ArrayList<RouterVo>();
+ RouterVo children = new RouterVo();
+ children.setPath(menu.getPath());
+ children.setComponent(menu.getComponent());
+ children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
+ children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+ children.setQuery(menu.getQuery());
+ childrenList.add(children);
+ router.setChildren(childrenList);
+ }
+ else if (menu.getParentId().intValue() == 0 && isInnerLink(menu))
+ {
+ router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
+ router.setPath("/");
+ List<RouterVo> childrenList = new ArrayList<RouterVo>();
+ RouterVo children = new RouterVo();
+ String routerPath = innerLinkReplaceEach(menu.getPath());
+ children.setPath(routerPath);
+ children.setComponent(UserConstants.INNER_LINK);
+ children.setName(getRouteName(menu.getRouteName(), routerPath));
+ children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
+ childrenList.add(children);
+ router.setChildren(childrenList);
+ }
+ routers.add(router);
+ }
+ return routers;
+ }
+
+ /**
+ * 构建前端所需要树结构
+ *
+ * @param menus 菜单列表
+ * @return 树结构列表
+ */
+ @Override
+ public List<SysMenu> buildMenuTree(List<SysMenu> menus)
+ {
+ List<SysMenu> returnList = new ArrayList<SysMenu>();
+ List<Long> tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());
+ for (Iterator<SysMenu> iterator = menus.iterator(); iterator.hasNext();)
+ {
+ SysMenu menu = (SysMenu) iterator.next();
+ // 如果是顶级节点, 遍历该父节点的所有子节点
+ if (!tempList.contains(menu.getParentId()))
+ {
+ recursionFn(menus, menu);
+ returnList.add(menu);
+ }
+ }
+ if (returnList.isEmpty())
+ {
+ returnList = menus;
+ }
+ return returnList;
+ }
+
+ /**
+ * 构建前端所需要下拉树结构
+ *
+ * @param menus 菜单列表
+ * @return 下拉树结构列表
+ */
+ @Override
+ public List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus)
+ {
+ List<SysMenu> menuTrees = buildMenuTree(menus);
+ return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
+ }
+
+ /**
+ * 根据菜单ID查询信息
+ *
+ * @param menuId 菜单ID
+ * @return 菜单信息
+ */
+ @Override
+ public SysMenu selectMenuById(Long menuId)
+ {
+ return menuMapper.selectMenuById(menuId);
+ }
+
+ /**
+ * 是否存在菜单子节点
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ @Override
+ public boolean hasChildByMenuId(Long menuId)
+ {
+ int result = menuMapper.hasChildByMenuId(menuId);
+ return result > 0;
+ }
+
+ /**
+ * 查询菜单使用数量
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ @Override
+ public boolean checkMenuExistRole(Long menuId)
+ {
+ int result = roleMenuMapper.checkMenuExistRole(menuId);
+ return result > 0;
+ }
+
+ /**
+ * 新增保存菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ @Override
+ public int insertMenu(SysMenu menu)
+ {
+ return menuMapper.insertMenu(menu);
+ }
+
+ /**
+ * 修改保存菜单信息
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ @Override
+ public int updateMenu(SysMenu menu)
+ {
+ return menuMapper.updateMenu(menu);
+ }
+
+ /**
+ * 删除菜单管理信息
+ *
+ * @param menuId 菜单ID
+ * @return 结果
+ */
+ @Override
+ public int deleteMenuById(Long menuId)
+ {
+ return menuMapper.deleteMenuById(menuId);
+ }
+
+ /**
+ * 校验菜单名称是否唯一
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkMenuNameUnique(SysMenu menu)
+ {
+ Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
+ SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId());
+ if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 获取路由名称
+ *
+ * @param menu 菜单信息
+ * @return 路由名称
+ */
+ public String getRouteName(SysMenu menu)
+ {
+ // 非外链并且是一级目录(类型为目录)
+ if (isMenuFrame(menu))
+ {
+ return StringUtils.EMPTY;
+ }
+ return getRouteName(menu.getRouteName(), menu.getPath());
+ }
+
+ /**
+ * 获取路由名称,如没有配置路由名称则取路由地址
+ *
+ * @param routerName 路由名称
+ * @param path 路由地址
+ * @return 路由名称(驼峰格式)
+ */
+ public String getRouteName(String name, String path)
+ {
+ String routerName = StringUtils.isNotEmpty(name) ? name : path;
+ return StringUtils.capitalize(routerName);
+ }
+
+ /**
+ * 获取路由地址
+ *
+ * @param menu 菜单信息
+ * @return 路由地址
+ */
+ public String getRouterPath(SysMenu menu)
+ {
+ String routerPath = menu.getPath();
+ // 内链打开外网方式
+ if (menu.getParentId().intValue() != 0 && isInnerLink(menu))
+ {
+ routerPath = innerLinkReplaceEach(routerPath);
+ }
+ // 非外链并且是一级目录(类型为目录)
+ if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
+ && UserConstants.NO_FRAME.equals(menu.getIsFrame()))
+ {
+ routerPath = "/" + menu.getPath();
+ }
+ // 非外链并且是一级目录(类型为菜单)
+ else if (isMenuFrame(menu))
+ {
+ routerPath = "/";
+ }
+ return routerPath;
+ }
+
+ /**
+ * 获取组件信息
+ *
+ * @param menu 菜单信息
+ * @return 组件信息
+ */
+ public String getComponent(SysMenu menu)
+ {
+ String component = UserConstants.LAYOUT;
+ if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu))
+ {
+ component = menu.getComponent();
+ }
+ else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu))
+ {
+ component = UserConstants.INNER_LINK;
+ }
+ else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu))
+ {
+ component = UserConstants.PARENT_VIEW;
+ }
+ return component;
+ }
+
+ /**
+ * 是否为菜单内部跳转
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public boolean isMenuFrame(SysMenu menu)
+ {
+ return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
+ && menu.getIsFrame().equals(UserConstants.NO_FRAME);
+ }
+
+ /**
+ * 是否为内链组件
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public boolean isInnerLink(SysMenu menu)
+ {
+ return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
+ }
+
+ /**
+ * 是否为parent_view组件
+ *
+ * @param menu 菜单信息
+ * @return 结果
+ */
+ public boolean isParentView(SysMenu menu)
+ {
+ return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
+ }
+
+ /**
+ * 根据父节点的ID获取所有子节点
+ *
+ * @param list 分类表
+ * @param parentId 传入的父节点ID
+ * @return String
+ */
+ public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
+ {
+ List<SysMenu> returnList = new ArrayList<SysMenu>();
+ for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();)
+ {
+ SysMenu t = (SysMenu) iterator.next();
+ // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
+ if (t.getParentId() == parentId)
+ {
+ recursionFn(list, t);
+ returnList.add(t);
+ }
+ }
+ return returnList;
+ }
+
+ /**
+ * 递归列表
+ *
+ * @param list 分类表
+ * @param t 子节点
+ */
+ private void recursionFn(List<SysMenu> list, SysMenu t)
+ {
+ // 得到子节点列表
+ List<SysMenu> childList = getChildList(list, t);
+ t.setChildren(childList);
+ for (SysMenu tChild : childList)
+ {
+ if (hasChild(list, tChild))
+ {
+ recursionFn(list, tChild);
+ }
+ }
+ }
+
+ /**
+ * 得到子节点列表
+ */
+ private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t)
+ {
+ List<SysMenu> tlist = new ArrayList<SysMenu>();
+ Iterator<SysMenu> it = list.iterator();
+ while (it.hasNext())
+ {
+ SysMenu n = (SysMenu) it.next();
+ if (n.getParentId().longValue() == t.getMenuId().longValue())
+ {
+ tlist.add(n);
+ }
+ }
+ return tlist;
+ }
+
+ /**
+ * 判断是否有子节点
+ */
+ private boolean hasChild(List<SysMenu> list, SysMenu t)
+ {
+ return getChildList(list, t).size() > 0;
+ }
+
+ /**
+ * 内链域名特殊字符替换
+ *
+ * @return 替换后的内链域名
+ */
+ public String innerLinkReplaceEach(String path)
+ {
+ return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" },
+ new String[] { "", "", "", "/", "/" });
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java
new file mode 100644
index 0000000..765438b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java
@@ -0,0 +1,92 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.domain.SysNotice;
+import com.ruoyi.system.mapper.SysNoticeMapper;
+import com.ruoyi.system.service.ISysNoticeService;
+
+/**
+ * 公告 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysNoticeServiceImpl implements ISysNoticeService
+{
+ @Autowired
+ private SysNoticeMapper noticeMapper;
+
+ /**
+ * 查询公告信息
+ *
+ * @param noticeId 公告ID
+ * @return 公告信息
+ */
+ @Override
+ public SysNotice selectNoticeById(Long noticeId)
+ {
+ return noticeMapper.selectNoticeById(noticeId);
+ }
+
+ /**
+ * 查询公告列表
+ *
+ * @param notice 公告信息
+ * @return 公告集合
+ */
+ @Override
+ public List<SysNotice> selectNoticeList(SysNotice notice)
+ {
+ return noticeMapper.selectNoticeList(notice);
+ }
+
+ /**
+ * 新增公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ @Override
+ public int insertNotice(SysNotice notice)
+ {
+ return noticeMapper.insertNotice(notice);
+ }
+
+ /**
+ * 修改公告
+ *
+ * @param notice 公告信息
+ * @return 结果
+ */
+ @Override
+ public int updateNotice(SysNotice notice)
+ {
+ return noticeMapper.updateNotice(notice);
+ }
+
+ /**
+ * 删除公告对象
+ *
+ * @param noticeId 公告ID
+ * @return 结果
+ */
+ @Override
+ public int deleteNoticeById(Long noticeId)
+ {
+ return noticeMapper.deleteNoticeById(noticeId);
+ }
+
+ /**
+ * 批量删除公告信息
+ *
+ * @param noticeIds 需要删除的公告ID
+ * @return 结果
+ */
+ @Override
+ public int deleteNoticeByIds(Long[] noticeIds)
+ {
+ return noticeMapper.deleteNoticeByIds(noticeIds);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java
new file mode 100644
index 0000000..5489815
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java
@@ -0,0 +1,76 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.domain.SysOperLog;
+import com.ruoyi.system.mapper.SysOperLogMapper;
+import com.ruoyi.system.service.ISysOperLogService;
+
+/**
+ * 操作日志 服务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysOperLogServiceImpl implements ISysOperLogService
+{
+ @Autowired
+ private SysOperLogMapper operLogMapper;
+
+ /**
+ * 新增操作日志
+ *
+ * @param operLog 操作日志对象
+ */
+ @Override
+ public void insertOperlog(SysOperLog operLog)
+ {
+ operLogMapper.insertOperlog(operLog);
+ }
+
+ /**
+ * 查询系统操作日志集合
+ *
+ * @param operLog 操作日志对象
+ * @return 操作日志集合
+ */
+ @Override
+ public List<SysOperLog> selectOperLogList(SysOperLog operLog)
+ {
+ return operLogMapper.selectOperLogList(operLog);
+ }
+
+ /**
+ * 批量删除系统操作日志
+ *
+ * @param operIds 需要删除的操作日志ID
+ * @return 结果
+ */
+ @Override
+ public int deleteOperLogByIds(Long[] operIds)
+ {
+ return operLogMapper.deleteOperLogByIds(operIds);
+ }
+
+ /**
+ * 查询操作日志详细
+ *
+ * @param operId 操作ID
+ * @return 操作日志对象
+ */
+ @Override
+ public SysOperLog selectOperLogById(Long operId)
+ {
+ return operLogMapper.selectOperLogById(operId);
+ }
+
+ /**
+ * 清空操作日志
+ */
+ @Override
+ public void cleanOperLog()
+ {
+ operLogMapper.cleanOperLog();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java
new file mode 100644
index 0000000..5e5fe06
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java
@@ -0,0 +1,178 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysPost;
+import com.ruoyi.system.mapper.SysPostMapper;
+import com.ruoyi.system.mapper.SysUserPostMapper;
+import com.ruoyi.system.service.ISysPostService;
+
+/**
+ * 岗位信息 服务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysPostServiceImpl implements ISysPostService
+{
+ @Autowired
+ private SysPostMapper postMapper;
+
+ @Autowired
+ private SysUserPostMapper userPostMapper;
+
+ /**
+ * 查询岗位信息集合
+ *
+ * @param post 岗位信息
+ * @return 岗位信息集合
+ */
+ @Override
+ public List<SysPost> selectPostList(SysPost post)
+ {
+ return postMapper.selectPostList(post);
+ }
+
+ /**
+ * 查询所有岗位
+ *
+ * @return 岗位列表
+ */
+ @Override
+ public List<SysPost> selectPostAll()
+ {
+ return postMapper.selectPostAll();
+ }
+
+ /**
+ * 通过岗位ID查询岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 角色对象信息
+ */
+ @Override
+ public SysPost selectPostById(Long postId)
+ {
+ return postMapper.selectPostById(postId);
+ }
+
+ /**
+ * 根据用户ID获取岗位选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中岗位ID列表
+ */
+ @Override
+ public List<Long> selectPostListByUserId(Long userId)
+ {
+ return postMapper.selectPostListByUserId(userId);
+ }
+
+ /**
+ * 校验岗位名称是否唯一
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkPostNameUnique(SysPost post)
+ {
+ Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
+ SysPost info = postMapper.checkPostNameUnique(post.getPostName());
+ if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验岗位编码是否唯一
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkPostCodeUnique(SysPost post)
+ {
+ Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
+ SysPost info = postMapper.checkPostCodeUnique(post.getPostCode());
+ if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 通过岗位ID查询岗位使用数量
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ @Override
+ public int countUserPostById(Long postId)
+ {
+ return userPostMapper.countUserPostById(postId);
+ }
+
+ /**
+ * 删除岗位信息
+ *
+ * @param postId 岗位ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostById(Long postId)
+ {
+ return postMapper.deletePostById(postId);
+ }
+
+ /**
+ * 批量删除岗位信息
+ *
+ * @param postIds 需要删除的岗位ID
+ * @return 结果
+ */
+ @Override
+ public int deletePostByIds(Long[] postIds)
+ {
+ for (Long postId : postIds)
+ {
+ SysPost post = selectPostById(postId);
+ if (countUserPostById(postId) > 0)
+ {
+ throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName()));
+ }
+ }
+ return postMapper.deletePostByIds(postIds);
+ }
+
+ /**
+ * 新增保存岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ @Override
+ public int insertPost(SysPost post)
+ {
+ return postMapper.insertPost(post);
+ }
+
+ /**
+ * 修改保存岗位信息
+ *
+ * @param post 岗位信息
+ * @return 结果
+ */
+ @Override
+ public int updatePost(SysPost post)
+ {
+ return postMapper.updatePost(post);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
new file mode 100644
index 0000000..e432bb1
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
@@ -0,0 +1,427 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.annotation.DataScope;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.SysRoleDept;
+import com.ruoyi.system.domain.SysRoleMenu;
+import com.ruoyi.system.domain.SysUserRole;
+import com.ruoyi.system.mapper.SysRoleDeptMapper;
+import com.ruoyi.system.mapper.SysRoleMapper;
+import com.ruoyi.system.mapper.SysRoleMenuMapper;
+import com.ruoyi.system.mapper.SysUserRoleMapper;
+import com.ruoyi.system.service.ISysRoleService;
+
+/**
+ * 角色 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysRoleServiceImpl implements ISysRoleService
+{
+ @Autowired
+ private SysRoleMapper roleMapper;
+
+ @Autowired
+ private SysRoleMenuMapper roleMenuMapper;
+
+ @Autowired
+ private SysUserRoleMapper userRoleMapper;
+
+ @Autowired
+ private SysRoleDeptMapper roleDeptMapper;
+
+ /**
+ * 根据条件分页查询角色数据
+ *
+ * @param role 角色信息
+ * @return 角色数据集合信息
+ */
+ @Override
+ @DataScope(deptAlias = "d")
+ public List<SysRole> selectRoleList(SysRole role)
+ {
+ return roleMapper.selectRoleList(role);
+ }
+
+ /**
+ * 根据用户ID查询角色
+ *
+ * @param userId 用户ID
+ * @return 角色列表
+ */
+ @Override
+ public List<SysRole> selectRolesByUserId(Long userId)
+ {
+ List<SysRole> userRoles = roleMapper.selectRolePermissionByUserId(userId);
+ List<SysRole> roles = selectRoleAll();
+ for (SysRole role : roles)
+ {
+ for (SysRole userRole : userRoles)
+ {
+ if (role.getRoleId().longValue() == userRole.getRoleId().longValue())
+ {
+ role.setFlag(true);
+ break;
+ }
+ }
+ }
+ return roles;
+ }
+
+ /**
+ * 根据用户ID查询权限
+ *
+ * @param userId 用户ID
+ * @return 权限列表
+ */
+ @Override
+ public Set<String> selectRolePermissionByUserId(Long userId)
+ {
+ List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);
+ Set<String> permsSet = new HashSet<>();
+ for (SysRole perm : perms)
+ {
+ if (StringUtils.isNotNull(perm))
+ {
+ permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
+ }
+ }
+ return permsSet;
+ }
+
+ /**
+ * 查询所有角色
+ *
+ * @return 角色列表
+ */
+ @Override
+ public List<SysRole> selectRoleAll()
+ {
+ return SpringUtils.getAopProxy(this).selectRoleList(new SysRole());
+ }
+
+ /**
+ * 根据用户ID获取角色选择框列表
+ *
+ * @param userId 用户ID
+ * @return 选中角色ID列表
+ */
+ @Override
+ public List<Long> selectRoleListByUserId(Long userId)
+ {
+ return roleMapper.selectRoleListByUserId(userId);
+ }
+
+ /**
+ * 通过角色ID查询角色
+ *
+ * @param roleId 角色ID
+ * @return 角色对象信息
+ */
+ @Override
+ public SysRole selectRoleById(Long roleId)
+ {
+ return roleMapper.selectRoleById(roleId);
+ }
+
+ /**
+ * 校验角色名称是否唯一
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkRoleNameUnique(SysRole role)
+ {
+ Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
+ SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName());
+ if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验角色权限是否唯一
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkRoleKeyUnique(SysRole role)
+ {
+ Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
+ SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey());
+ if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验角色是否允许操作
+ *
+ * @param role 角色信息
+ */
+ @Override
+ public void checkRoleAllowed(SysRole role)
+ {
+ if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin())
+ {
+ throw new ServiceException("不允许操作超级管理员角色");
+ }
+ }
+
+ /**
+ * 校验角色是否有数据权限
+ *
+ * @param roleIds 角色id
+ */
+ @Override
+ public void checkRoleDataScope(Long... roleIds)
+ {
+ if (!SysUser.isAdmin(SecurityUtils.getUserId()))
+ {
+ for (Long roleId : roleIds)
+ {
+ SysRole role = new SysRole();
+ role.setRoleId(roleId);
+ List<SysRole> roles = SpringUtils.getAopProxy(this).selectRoleList(role);
+ if (StringUtils.isEmpty(roles))
+ {
+ throw new ServiceException("没有权限访问角色数据!");
+ }
+ }
+ }
+ }
+
+ /**
+ * 通过角色ID查询角色使用数量
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ @Override
+ public int countUserRoleByRoleId(Long roleId)
+ {
+ return userRoleMapper.countUserRoleByRoleId(roleId);
+ }
+
+ /**
+ * 新增保存角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int insertRole(SysRole role)
+ {
+ // 新增角色信息
+ roleMapper.insertRole(role);
+ return insertRoleMenu(role);
+ }
+
+ /**
+ * 修改保存角色信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int updateRole(SysRole role)
+ {
+ // 修改角色信息
+ roleMapper.updateRole(role);
+ // 删除角色与菜单关联
+ roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId());
+ return insertRoleMenu(role);
+ }
+
+ /**
+ * 修改角色状态
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ public int updateRoleStatus(SysRole role)
+ {
+ return roleMapper.updateRole(role);
+ }
+
+ /**
+ * 修改数据权限信息
+ *
+ * @param role 角色信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int authDataScope(SysRole role)
+ {
+ // 修改角色信息
+ roleMapper.updateRole(role);
+ // 删除角色与部门关联
+ roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId());
+ // 新增角色和部门信息(数据权限)
+ return insertRoleDept(role);
+ }
+
+ /**
+ * 新增角色菜单信息
+ *
+ * @param role 角色对象
+ */
+ public int insertRoleMenu(SysRole role)
+ {
+ int rows = 1;
+ // 新增用户与角色管理
+ List<SysRoleMenu> list = new ArrayList<SysRoleMenu>();
+ for (Long menuId : role.getMenuIds())
+ {
+ SysRoleMenu rm = new SysRoleMenu();
+ rm.setRoleId(role.getRoleId());
+ rm.setMenuId(menuId);
+ list.add(rm);
+ }
+ if (list.size() > 0)
+ {
+ rows = roleMenuMapper.batchRoleMenu(list);
+ }
+ return rows;
+ }
+
+ /**
+ * 新增角色部门信息(数据权限)
+ *
+ * @param role 角色对象
+ */
+ public int insertRoleDept(SysRole role)
+ {
+ int rows = 1;
+ // 新增角色与部门(数据权限)管理
+ List<SysRoleDept> list = new ArrayList<SysRoleDept>();
+ for (Long deptId : role.getDeptIds())
+ {
+ SysRoleDept rd = new SysRoleDept();
+ rd.setRoleId(role.getRoleId());
+ rd.setDeptId(deptId);
+ list.add(rd);
+ }
+ if (list.size() > 0)
+ {
+ rows = roleDeptMapper.batchRoleDept(list);
+ }
+ return rows;
+ }
+
+ /**
+ * 通过角色ID删除角色
+ *
+ * @param roleId 角色ID
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int deleteRoleById(Long roleId)
+ {
+ // 删除角色与菜单关联
+ roleMenuMapper.deleteRoleMenuByRoleId(roleId);
+ // 删除角色与部门关联
+ roleDeptMapper.deleteRoleDeptByRoleId(roleId);
+ return roleMapper.deleteRoleById(roleId);
+ }
+
+ /**
+ * 批量删除角色信息
+ *
+ * @param roleIds 需要删除的角色ID
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int deleteRoleByIds(Long[] roleIds)
+ {
+ for (Long roleId : roleIds)
+ {
+ checkRoleAllowed(new SysRole(roleId));
+ checkRoleDataScope(roleId);
+ SysRole role = selectRoleById(roleId);
+ if (countUserRoleByRoleId(roleId) > 0)
+ {
+ throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
+ }
+ }
+ // 删除角色与菜单关联
+ roleMenuMapper.deleteRoleMenu(roleIds);
+ // 删除角色与部门关联
+ roleDeptMapper.deleteRoleDept(roleIds);
+ return roleMapper.deleteRoleByIds(roleIds);
+ }
+
+ /**
+ * 取消授权用户角色
+ *
+ * @param userRole 用户和角色关联信息
+ * @return 结果
+ */
+ @Override
+ public int deleteAuthUser(SysUserRole userRole)
+ {
+ return userRoleMapper.deleteUserRoleInfo(userRole);
+ }
+
+ /**
+ * 批量取消授权用户角色
+ *
+ * @param roleId 角色ID
+ * @param userIds 需要取消授权的用户数据ID
+ * @return 结果
+ */
+ @Override
+ public int deleteAuthUsers(Long roleId, Long[] userIds)
+ {
+ return userRoleMapper.deleteUserRoleInfos(roleId, userIds);
+ }
+
+ /**
+ * 批量选择授权用户角色
+ *
+ * @param roleId 角色ID
+ * @param userIds 需要授权的用户数据ID
+ * @return 结果
+ */
+ @Override
+ public int insertAuthUsers(Long roleId, Long[] userIds)
+ {
+ // 新增用户与角色管理
+ List<SysUserRole> list = new ArrayList<SysUserRole>();
+ for (Long userId : userIds)
+ {
+ SysUserRole ur = new SysUserRole();
+ ur.setUserId(userId);
+ ur.setRoleId(roleId);
+ list.add(ur);
+ }
+ return userRoleMapper.batchUserRole(list);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java
new file mode 100644
index 0000000..f80a877
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java
@@ -0,0 +1,96 @@
+package com.ruoyi.system.service.impl;
+
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysUserOnline;
+import com.ruoyi.system.service.ISysUserOnlineService;
+
+/**
+ * 在线用户 服务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysUserOnlineServiceImpl implements ISysUserOnlineService
+{
+ /**
+ * 通过登录地址查询信息
+ *
+ * @param ipaddr 登录地址
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ @Override
+ public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user)
+ {
+ if (StringUtils.equals(ipaddr, user.getIpaddr()))
+ {
+ return loginUserToUserOnline(user);
+ }
+ return null;
+ }
+
+ /**
+ * 通过用户名称查询信息
+ *
+ * @param userName 用户名称
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ @Override
+ public SysUserOnline selectOnlineByUserName(String userName, LoginUser user)
+ {
+ if (StringUtils.equals(userName, user.getUsername()))
+ {
+ return loginUserToUserOnline(user);
+ }
+ return null;
+ }
+
+ /**
+ * 通过登录地址/用户名称查询信息
+ *
+ * @param ipaddr 登录地址
+ * @param userName 用户名称
+ * @param user 用户信息
+ * @return 在线用户信息
+ */
+ @Override
+ public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user)
+ {
+ if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername()))
+ {
+ return loginUserToUserOnline(user);
+ }
+ return null;
+ }
+
+ /**
+ * 设置在线用户信息
+ *
+ * @param user 用户信息
+ * @return 在线用户
+ */
+ @Override
+ public SysUserOnline loginUserToUserOnline(LoginUser user)
+ {
+ if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser()))
+ {
+ return null;
+ }
+ SysUserOnline sysUserOnline = new SysUserOnline();
+ sysUserOnline.setTokenId(user.getToken());
+ sysUserOnline.setUserName(user.getUsername());
+ sysUserOnline.setIpaddr(user.getIpaddr());
+ sysUserOnline.setLoginLocation(user.getLoginLocation());
+ sysUserOnline.setBrowser(user.getBrowser());
+ sysUserOnline.setOs(user.getOs());
+ sysUserOnline.setLoginTime(user.getLoginTime());
+ if (StringUtils.isNotNull(user.getUser().getDept()))
+ {
+ sysUserOnline.setDeptName(user.getUser().getDept().getDeptName());
+ }
+ return sysUserOnline;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
new file mode 100644
index 0000000..82c303a
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
@@ -0,0 +1,550 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import jakarta.validation.Validator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import com.ruoyi.common.annotation.DataScope;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.bean.BeanValidators;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.SysPost;
+import com.ruoyi.system.domain.SysUserPost;
+import com.ruoyi.system.domain.SysUserRole;
+import com.ruoyi.system.mapper.SysPostMapper;
+import com.ruoyi.system.mapper.SysRoleMapper;
+import com.ruoyi.system.mapper.SysUserMapper;
+import com.ruoyi.system.mapper.SysUserPostMapper;
+import com.ruoyi.system.mapper.SysUserRoleMapper;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 用户 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysUserServiceImpl implements ISysUserService
+{
+ private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
+
+ @Autowired
+ private SysUserMapper userMapper;
+
+ @Autowired
+ private SysRoleMapper roleMapper;
+
+ @Autowired
+ private SysPostMapper postMapper;
+
+ @Autowired
+ private SysUserRoleMapper userRoleMapper;
+
+ @Autowired
+ private SysUserPostMapper userPostMapper;
+
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private ISysDeptService deptService;
+
+ @Autowired
+ protected Validator validator;
+
+ /**
+ * 根据条件分页查询用户列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ @Override
+ @DataScope(deptAlias = "d", userAlias = "u")
+ public List<SysUser> selectUserList(SysUser user)
+ {
+ return userMapper.selectUserList(user);
+ }
+
+ /**
+ * 根据条件分页查询已分配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ @Override
+ @DataScope(deptAlias = "d", userAlias = "u")
+ public List<SysUser> selectAllocatedList(SysUser user)
+ {
+ return userMapper.selectAllocatedList(user);
+ }
+
+ /**
+ * 根据条件分页查询未分配用户角色列表
+ *
+ * @param user 用户信息
+ * @return 用户信息集合信息
+ */
+ @Override
+ @DataScope(deptAlias = "d", userAlias = "u")
+ public List<SysUser> selectUnallocatedList(SysUser user)
+ {
+ return userMapper.selectUnallocatedList(user);
+ }
+
+ /**
+ * 通过用户名查询用户
+ *
+ * @param userName 用户名
+ * @return 用户对象信息
+ */
+ @Override
+ public SysUser selectUserByUserName(String userName)
+ {
+ return userMapper.selectUserByUserName(userName);
+ }
+
+ /**
+ * 通过用户ID查询用户
+ *
+ * @param userId 用户ID
+ * @return 用户对象信息
+ */
+ @Override
+ public SysUser selectUserById(Long userId)
+ {
+ return userMapper.selectUserById(userId);
+ }
+
+ /**
+ * 查询用户所属角色组
+ *
+ * @param userName 用户名
+ * @return 结果
+ */
+ @Override
+ public String selectUserRoleGroup(String userName)
+ {
+ List<SysRole> list = roleMapper.selectRolesByUserName(userName);
+ if (CollectionUtils.isEmpty(list))
+ {
+ return StringUtils.EMPTY;
+ }
+ return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(","));
+ }
+
+ /**
+ * 查询用户所属岗位组
+ *
+ * @param userName 用户名
+ * @return 结果
+ */
+ @Override
+ public String selectUserPostGroup(String userName)
+ {
+ List<SysPost> list = postMapper.selectPostsByUserName(userName);
+ if (CollectionUtils.isEmpty(list))
+ {
+ return StringUtils.EMPTY;
+ }
+ return list.stream().map(SysPost::getPostName).collect(Collectors.joining(","));
+ }
+
+ /**
+ * 校验用户名称是否唯一
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ public boolean checkUserNameUnique(SysUser user)
+ {
+ Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+ SysUser info = userMapper.checkUserNameUnique(user.getUserName());
+ if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验手机号码是否唯一
+ *
+ * @param user 用户信息
+ * @return
+ */
+ @Override
+ public boolean checkPhoneUnique(SysUser user)
+ {
+ Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+ SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
+ if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验email是否唯一
+ *
+ * @param user 用户信息
+ * @return
+ */
+ @Override
+ public boolean checkEmailUnique(SysUser user)
+ {
+ Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+ SysUser info = userMapper.checkEmailUnique(user.getEmail());
+ if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
+ {
+ return UserConstants.NOT_UNIQUE;
+ }
+ return UserConstants.UNIQUE;
+ }
+
+ /**
+ * 校验用户是否允许操作
+ *
+ * @param user 用户信息
+ */
+ @Override
+ public void checkUserAllowed(SysUser user)
+ {
+ if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin())
+ {
+ throw new ServiceException("不允许操作超级管理员用户");
+ }
+ }
+
+ /**
+ * 校验用户是否有数据权限
+ *
+ * @param userId 用户id
+ */
+ @Override
+ public void checkUserDataScope(Long userId)
+ {
+ if (!SysUser.isAdmin(SecurityUtils.getUserId()))
+ {
+ SysUser user = new SysUser();
+ user.setUserId(userId);
+ List<SysUser> users = SpringUtils.getAopProxy(this).selectUserList(user);
+ if (StringUtils.isEmpty(users))
+ {
+ throw new ServiceException("没有权限访问用户数据!");
+ }
+ }
+ }
+
+ /**
+ * 新增保存用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int insertUser(SysUser user)
+ {
+ // 新增用户信息
+ int rows = userMapper.insertUser(user);
+ // 新增用户岗位关联
+ insertUserPost(user);
+ // 新增用户与角色管理
+ insertUserRole(user);
+ return rows;
+ }
+
+ /**
+ * 注册用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ public boolean registerUser(SysUser user)
+ {
+ return userMapper.insertUser(user) > 0;
+ }
+
+ /**
+ * 修改保存用户信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int updateUser(SysUser user)
+ {
+ Long userId = user.getUserId();
+ // 删除用户与角色关联
+ userRoleMapper.deleteUserRoleByUserId(userId);
+ // 新增用户与角色管理
+ insertUserRole(user);
+ // 删除用户与岗位关联
+ userPostMapper.deleteUserPostByUserId(userId);
+ // 新增用户与岗位管理
+ insertUserPost(user);
+ return userMapper.updateUser(user);
+ }
+
+ /**
+ * 用户授权角色
+ *
+ * @param userId 用户ID
+ * @param roleIds 角色组
+ */
+ @Override
+ @Transactional
+ public void insertUserAuth(Long userId, Long[] roleIds)
+ {
+ userRoleMapper.deleteUserRoleByUserId(userId);
+ insertUserRole(userId, roleIds);
+ }
+
+ /**
+ * 修改用户状态
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ public int updateUserStatus(SysUser user)
+ {
+ return userMapper.updateUser(user);
+ }
+
+ /**
+ * 修改用户基本信息
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ public int updateUserProfile(SysUser user)
+ {
+ return userMapper.updateUser(user);
+ }
+
+ /**
+ * 修改用户头像
+ *
+ * @param userName 用户名
+ * @param avatar 头像地址
+ * @return 结果
+ */
+ @Override
+ public boolean updateUserAvatar(String userName, String avatar)
+ {
+ return userMapper.updateUserAvatar(userName, avatar) > 0;
+ }
+
+ /**
+ * 重置用户密码
+ *
+ * @param user 用户信息
+ * @return 结果
+ */
+ @Override
+ public int resetPwd(SysUser user)
+ {
+ return userMapper.updateUser(user);
+ }
+
+ /**
+ * 重置用户密码
+ *
+ * @param userName 用户名
+ * @param password 密码
+ * @return 结果
+ */
+ @Override
+ public int resetUserPwd(String userName, String password)
+ {
+ return userMapper.resetUserPwd(userName, password);
+ }
+
+ /**
+ * 新增用户角色信息
+ *
+ * @param user 用户对象
+ */
+ public void insertUserRole(SysUser user)
+ {
+ this.insertUserRole(user.getUserId(), user.getRoleIds());
+ }
+
+ /**
+ * 新增用户岗位信息
+ *
+ * @param user 用户对象
+ */
+ public void insertUserPost(SysUser user)
+ {
+ Long[] posts = user.getPostIds();
+ if (StringUtils.isNotEmpty(posts))
+ {
+ // 新增用户与岗位管理
+ List<SysUserPost> list = new ArrayList<SysUserPost>(posts.length);
+ for (Long postId : posts)
+ {
+ SysUserPost up = new SysUserPost();
+ up.setUserId(user.getUserId());
+ up.setPostId(postId);
+ list.add(up);
+ }
+ userPostMapper.batchUserPost(list);
+ }
+ }
+
+ /**
+ * 新增用户角色信息
+ *
+ * @param userId 用户ID
+ * @param roleIds 角色组
+ */
+ public void insertUserRole(Long userId, Long[] roleIds)
+ {
+ if (StringUtils.isNotEmpty(roleIds))
+ {
+ // 新增用户与角色管理
+ List<SysUserRole> list = new ArrayList<SysUserRole>(roleIds.length);
+ for (Long roleId : roleIds)
+ {
+ SysUserRole ur = new SysUserRole();
+ ur.setUserId(userId);
+ ur.setRoleId(roleId);
+ list.add(ur);
+ }
+ userRoleMapper.batchUserRole(list);
+ }
+ }
+
+ /**
+ * 通过用户ID删除用户
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int deleteUserById(Long userId)
+ {
+ // 删除用户与角色关联
+ userRoleMapper.deleteUserRoleByUserId(userId);
+ // 删除用户与岗位表
+ userPostMapper.deleteUserPostByUserId(userId);
+ return userMapper.deleteUserById(userId);
+ }
+
+ /**
+ * 批量删除用户信息
+ *
+ * @param userIds 需要删除的用户ID
+ * @return 结果
+ */
+ @Override
+ @Transactional
+ public int deleteUserByIds(Long[] userIds)
+ {
+ for (Long userId : userIds)
+ {
+ checkUserAllowed(new SysUser(userId));
+ checkUserDataScope(userId);
+ }
+ // 删除用户与角色关联
+ userRoleMapper.deleteUserRole(userIds);
+ // 删除用户与岗位关联
+ userPostMapper.deleteUserPost(userIds);
+ return userMapper.deleteUserByIds(userIds);
+ }
+
+ /**
+ * 导入用户数据
+ *
+ * @param userList 用户数据列表
+ * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+ * @param operName 操作用户
+ * @return 结果
+ */
+ @Override
+ public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName)
+ {
+ if (StringUtils.isNull(userList) || userList.size() == 0)
+ {
+ throw new ServiceException("导入用户数据不能为空!");
+ }
+ int successNum = 0;
+ int failureNum = 0;
+ StringBuilder successMsg = new StringBuilder();
+ StringBuilder failureMsg = new StringBuilder();
+ for (SysUser user : userList)
+ {
+ try
+ {
+ // 验证是否存在这个用户
+ SysUser u = userMapper.selectUserByUserName(user.getUserName());
+ if (StringUtils.isNull(u))
+ {
+ BeanValidators.validateWithException(validator, user);
+ deptService.checkDeptDataScope(user.getDeptId());
+ String password = configService.selectConfigByKey("sys.user.initPassword");
+ user.setPassword(SecurityUtils.encryptPassword(password));
+ user.setCreateBy(operName);
+ userMapper.insertUser(user);
+ successNum++;
+ successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
+ }
+ else if (isUpdateSupport)
+ {
+ BeanValidators.validateWithException(validator, user);
+ checkUserAllowed(u);
+ checkUserDataScope(u.getUserId());
+ deptService.checkDeptDataScope(user.getDeptId());
+ user.setUserId(u.getUserId());
+ user.setUpdateBy(operName);
+ userMapper.updateUser(user);
+ successNum++;
+ successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
+ }
+ else
+ {
+ failureNum++;
+ failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
+ }
+ }
+ catch (Exception e)
+ {
+ failureNum++;
+ String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
+ failureMsg.append(msg + e.getMessage());
+ log.error(msg, e);
+ }
+ }
+ if (failureNum > 0)
+ {
+ failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+ throw new ServiceException(failureMsg.toString());
+ }
+ else
+ {
+ successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+ }
+ return successMsg.toString();
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml
new file mode 100644
index 0000000..a5ff114
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysConfigMapper">
+
+ <resultMap type="SysConfig" id="SysConfigResult">
+ <id property="configId" column="config_id" />
+ <result property="configName" column="config_name" />
+ <result property="configKey" column="config_key" />
+ <result property="configValue" column="config_value" />
+ <result property="configType" column="config_type" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectConfigVo">
+ select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark
+ from sys_config
+ </sql>
+
+ <!-- 查询条件 -->
+ <sql id="sqlwhereSearch">
+ <where>
+ <if test="configId !=null">
+ and config_id = #{configId}
+ </if>
+ <if test="configKey !=null and configKey != ''">
+ and config_key = #{configKey}
+ </if>
+ </where>
+ </sql>
+
+ <select id="selectConfig" parameterType="SysConfig" resultMap="SysConfigResult">
+ <include refid="selectConfigVo"/>
+ <include refid="sqlwhereSearch"/>
+ </select>
+
+ <select id="selectConfigList" parameterType="SysConfig" resultMap="SysConfigResult">
+ <include refid="selectConfigVo"/>
+ <where>
+ <if test="configName != null and configName != ''">
+ AND config_name like concat('%', #{configName}, '%')
+ </if>
+ <if test="configType != null and configType != ''">
+ AND config_type = #{configType}
+ </if>
+ <if test="configKey != null and configKey != ''">
+ AND config_key like concat('%', #{configKey}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ and date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ </where>
+ </select>
+
+ <select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult">
+ <include refid="selectConfigVo"/>
+ where config_id = #{configId}
+ </select>
+
+ <select id="checkConfigKeyUnique" parameterType="String" resultMap="SysConfigResult">
+ <include refid="selectConfigVo"/>
+ where config_key = #{configKey} limit 1
+ </select>
+
+ <insert id="insertConfig" parameterType="SysConfig">
+ insert into sys_config (
+ <if test="configName != null and configName != '' ">config_name,</if>
+ <if test="configKey != null and configKey != '' ">config_key,</if>
+ <if test="configValue != null and configValue != '' ">config_value,</if>
+ <if test="configType != null and configType != '' ">config_type,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ create_time
+ )values(
+ <if test="configName != null and configName != ''">#{configName},</if>
+ <if test="configKey != null and configKey != ''">#{configKey},</if>
+ <if test="configValue != null and configValue != ''">#{configValue},</if>
+ <if test="configType != null and configType != ''">#{configType},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateConfig" parameterType="SysConfig">
+ update sys_config
+ <set>
+ <if test="configName != null and configName != ''">config_name = #{configName},</if>
+ <if test="configKey != null and configKey != ''">config_key = #{configKey},</if>
+ <if test="configValue != null and configValue != ''">config_value = #{configValue},</if>
+ <if test="configType != null and configType != ''">config_type = #{configType},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ update_time = sysdate()
+ </set>
+ where config_id = #{configId}
+ </update>
+
+ <delete id="deleteConfigById" parameterType="Long">
+ delete from sys_config where config_id = #{configId}
+ </delete>
+
+ <delete id="deleteConfigByIds" parameterType="Long">
+ delete from sys_config where config_id in
+ <foreach item="configId" collection="array" open="(" separator="," close=")">
+ #{configId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
new file mode 100644
index 0000000..cf439f6
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysDeptMapper">
+
+ <resultMap type="SysDept" id="SysDeptResult">
+ <id property="deptId" column="dept_id" />
+ <result property="parentId" column="parent_id" />
+ <result property="ancestors" column="ancestors" />
+ <result property="deptName" column="dept_name" />
+ <result property="orderNum" column="order_num" />
+ <result property="leader" column="leader" />
+ <result property="phone" column="phone" />
+ <result property="email" column="email" />
+ <result property="status" column="status" />
+ <result property="delFlag" column="del_flag" />
+ <result property="parentName" column="parent_name" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectDeptVo">
+ select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time
+ from sys_dept d
+ </sql>
+
+ <select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
+ <include refid="selectDeptVo"/>
+ where d.del_flag = '0'
+ <if test="deptId != null and deptId != 0">
+ AND dept_id = #{deptId}
+ </if>
+ <if test="parentId != null and parentId != 0">
+ AND parent_id = #{parentId}
+ </if>
+ <if test="deptName != null and deptName != ''">
+ AND dept_name like concat('%', #{deptName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <!-- 数据范围过滤 -->
+ ${params.dataScope}
+ order by d.parent_id, d.order_num
+ </select>
+
+ <select id="selectDeptListByRoleId" resultType="Long">
+ select d.dept_id
+ from sys_dept d
+ left join sys_role_dept rd on d.dept_id = rd.dept_id
+ where rd.role_id = #{roleId}
+ <if test="deptCheckStrictly">
+ and d.dept_id not in (select d.parent_id from sys_dept d inner join sys_role_dept rd on d.dept_id = rd.dept_id and rd.role_id = #{roleId})
+ </if>
+ order by d.parent_id, d.order_num
+ </select>
+
+ <select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
+ select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status,
+ (select dept_name from sys_dept where dept_id = d.parent_id) parent_name
+ from sys_dept d
+ where d.dept_id = #{deptId}
+ </select>
+
+ <select id="checkDeptExistUser" parameterType="Long" resultType="int">
+ select count(1) from sys_user where dept_id = #{deptId} and del_flag = '0'
+ </select>
+
+ <select id="hasChildByDeptId" parameterType="Long" resultType="int">
+ select count(1) from sys_dept
+ where del_flag = '0' and parent_id = #{deptId} limit 1
+ </select>
+
+ <select id="selectChildrenDeptById" parameterType="Long" resultMap="SysDeptResult">
+ select * from sys_dept where find_in_set(#{deptId}, ancestors)
+ </select>
+
+ <select id="selectNormalChildrenDeptById" parameterType="Long" resultType="int">
+ select count(*) from sys_dept where status = 0 and del_flag = '0' and find_in_set(#{deptId}, ancestors)
+ </select>
+
+ <select id="checkDeptNameUnique" resultMap="SysDeptResult">
+ <include refid="selectDeptVo"/>
+ where dept_name=#{deptName} and parent_id = #{parentId} and del_flag = '0' limit 1
+ </select>
+
+ <insert id="insertDept" parameterType="SysDept">
+ insert into sys_dept(
+ <if test="deptId != null and deptId != 0">dept_id,</if>
+ <if test="parentId != null and parentId != 0">parent_id,</if>
+ <if test="deptName != null and deptName != ''">dept_name,</if>
+ <if test="ancestors != null and ancestors != ''">ancestors,</if>
+ <if test="orderNum != null">order_num,</if>
+ <if test="leader != null and leader != ''">leader,</if>
+ <if test="phone != null and phone != ''">phone,</if>
+ <if test="email != null and email != ''">email,</if>
+ <if test="status != null">status,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="deptId != null and deptId != 0">#{deptId},</if>
+ <if test="parentId != null and parentId != 0">#{parentId},</if>
+ <if test="deptName != null and deptName != ''">#{deptName},</if>
+ <if test="ancestors != null and ancestors != ''">#{ancestors},</if>
+ <if test="orderNum != null">#{orderNum},</if>
+ <if test="leader != null and leader != ''">#{leader},</if>
+ <if test="phone != null and phone != ''">#{phone},</if>
+ <if test="email != null and email != ''">#{email},</if>
+ <if test="status != null">#{status},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateDept" parameterType="SysDept">
+ update sys_dept
+ <set>
+ <if test="parentId != null and parentId != 0">parent_id = #{parentId},</if>
+ <if test="deptName != null and deptName != ''">dept_name = #{deptName},</if>
+ <if test="ancestors != null and ancestors != ''">ancestors = #{ancestors},</if>
+ <if test="orderNum != null">order_num = #{orderNum},</if>
+ <if test="leader != null">leader = #{leader},</if>
+ <if test="phone != null">phone = #{phone},</if>
+ <if test="email != null">email = #{email},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where dept_id = #{deptId}
+ </update>
+
+ <update id="updateDeptChildren" parameterType="java.util.List">
+ update sys_dept set ancestors =
+ <foreach collection="depts" item="item" index="index"
+ separator=" " open="case dept_id" close="end">
+ when #{item.deptId} then #{item.ancestors}
+ </foreach>
+ where dept_id in
+ <foreach collection="depts" item="item" index="index"
+ separator="," open="(" close=")">
+ #{item.deptId}
+ </foreach>
+ </update>
+
+ <update id="updateDeptStatusNormal" parameterType="Long">
+ update sys_dept set status = '0' where dept_id in
+ <foreach collection="array" item="deptId" open="(" separator="," close=")">
+ #{deptId}
+ </foreach>
+ </update>
+
+ <delete id="deleteDeptById" parameterType="Long">
+ update sys_dept set del_flag = '2' where dept_id = #{deptId}
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml
new file mode 100644
index 0000000..3b94b7f
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysDictDataMapper">
+
+ <resultMap type="SysDictData" id="SysDictDataResult">
+ <id property="dictCode" column="dict_code" />
+ <result property="dictSort" column="dict_sort" />
+ <result property="dictLabel" column="dict_label" />
+ <result property="dictValue" column="dict_value" />
+ <result property="dictType" column="dict_type" />
+ <result property="cssClass" column="css_class" />
+ <result property="listClass" column="list_class" />
+ <result property="isDefault" column="is_default" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectDictDataVo">
+ select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark
+ from sys_dict_data
+ </sql>
+
+ <select id="selectDictDataList" parameterType="SysDictData" resultMap="SysDictDataResult">
+ <include refid="selectDictDataVo"/>
+ <where>
+ <if test="dictType != null and dictType != ''">
+ AND dict_type = #{dictType}
+ </if>
+ <if test="dictLabel != null and dictLabel != ''">
+ AND dict_label like concat('%', #{dictLabel}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by dict_sort asc
+ </select>
+
+ <select id="selectDictDataByType" parameterType="String" resultMap="SysDictDataResult">
+ <include refid="selectDictDataVo"/>
+ where status = '0' and dict_type = #{dictType} order by dict_sort asc
+ </select>
+
+ <select id="selectDictLabel" resultType="String">
+ select dict_label from sys_dict_data
+ where dict_type = #{dictType} and dict_value = #{dictValue}
+ </select>
+
+ <select id="selectDictDataById" parameterType="Long" resultMap="SysDictDataResult">
+ <include refid="selectDictDataVo"/>
+ where dict_code = #{dictCode}
+ </select>
+
+ <select id="countDictDataByType" resultType="Integer">
+ select count(1) from sys_dict_data where dict_type=#{dictType}
+ </select>
+
+ <delete id="deleteDictDataById" parameterType="Long">
+ delete from sys_dict_data where dict_code = #{dictCode}
+ </delete>
+
+ <delete id="deleteDictDataByIds" parameterType="Long">
+ delete from sys_dict_data where dict_code in
+ <foreach collection="array" item="dictCode" open="(" separator="," close=")">
+ #{dictCode}
+ </foreach>
+ </delete>
+
+ <update id="updateDictData" parameterType="SysDictData">
+ update sys_dict_data
+ <set>
+ <if test="dictSort != null">dict_sort = #{dictSort},</if>
+ <if test="dictLabel != null and dictLabel != ''">dict_label = #{dictLabel},</if>
+ <if test="dictValue != null and dictValue != ''">dict_value = #{dictValue},</if>
+ <if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
+ <if test="cssClass != null">css_class = #{cssClass},</if>
+ <if test="listClass != null">list_class = #{listClass},</if>
+ <if test="isDefault != null and isDefault != ''">is_default = #{isDefault},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where dict_code = #{dictCode}
+ </update>
+
+ <update id="updateDictDataType" parameterType="String">
+ update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType}
+ </update>
+
+ <insert id="insertDictData" parameterType="SysDictData">
+ insert into sys_dict_data(
+ <if test="dictSort != null">dict_sort,</if>
+ <if test="dictLabel != null and dictLabel != ''">dict_label,</if>
+ <if test="dictValue != null and dictValue != ''">dict_value,</if>
+ <if test="dictType != null and dictType != ''">dict_type,</if>
+ <if test="cssClass != null and cssClass != ''">css_class,</if>
+ <if test="listClass != null and listClass != ''">list_class,</if>
+ <if test="isDefault != null and isDefault != ''">is_default,</if>
+ <if test="status != null">status,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="dictSort != null">#{dictSort},</if>
+ <if test="dictLabel != null and dictLabel != ''">#{dictLabel},</if>
+ <if test="dictValue != null and dictValue != ''">#{dictValue},</if>
+ <if test="dictType != null and dictType != ''">#{dictType},</if>
+ <if test="cssClass != null and cssClass != ''">#{cssClass},</if>
+ <if test="listClass != null and listClass != ''">#{listClass},</if>
+ <if test="isDefault != null and isDefault != ''">#{isDefault},</if>
+ <if test="status != null">#{status},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml
new file mode 100644
index 0000000..438d484
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysDictTypeMapper">
+
+ <resultMap type="SysDictType" id="SysDictTypeResult">
+ <id property="dictId" column="dict_id" />
+ <result property="dictName" column="dict_name" />
+ <result property="dictType" column="dict_type" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectDictTypeVo">
+ select dict_id, dict_name, dict_type, status, create_by, create_time, remark
+ from sys_dict_type
+ </sql>
+
+ <select id="selectDictTypeList" parameterType="SysDictType" resultMap="SysDictTypeResult">
+ <include refid="selectDictTypeVo"/>
+ <where>
+ <if test="dictName != null and dictName != ''">
+ AND dict_name like concat('%', #{dictName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="dictType != null and dictType != ''">
+ AND dict_type like concat('%', #{dictType}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ and date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ </where>
+ </select>
+
+ <select id="selectDictTypeAll" resultMap="SysDictTypeResult">
+ <include refid="selectDictTypeVo"/>
+ </select>
+
+ <select id="selectDictTypeById" parameterType="Long" resultMap="SysDictTypeResult">
+ <include refid="selectDictTypeVo"/>
+ where dict_id = #{dictId}
+ </select>
+
+ <select id="selectDictTypeByType" parameterType="String" resultMap="SysDictTypeResult">
+ <include refid="selectDictTypeVo"/>
+ where dict_type = #{dictType}
+ </select>
+
+ <select id="checkDictTypeUnique" parameterType="String" resultMap="SysDictTypeResult">
+ <include refid="selectDictTypeVo"/>
+ where dict_type = #{dictType} limit 1
+ </select>
+
+ <delete id="deleteDictTypeById" parameterType="Long">
+ delete from sys_dict_type where dict_id = #{dictId}
+ </delete>
+
+ <delete id="deleteDictTypeByIds" parameterType="Long">
+ delete from sys_dict_type where dict_id in
+ <foreach collection="array" item="dictId" open="(" separator="," close=")">
+ #{dictId}
+ </foreach>
+ </delete>
+
+ <update id="updateDictType" parameterType="SysDictType">
+ update sys_dict_type
+ <set>
+ <if test="dictName != null and dictName != ''">dict_name = #{dictName},</if>
+ <if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where dict_id = #{dictId}
+ </update>
+
+ <insert id="insertDictType" parameterType="SysDictType">
+ insert into sys_dict_type(
+ <if test="dictName != null and dictName != ''">dict_name,</if>
+ <if test="dictType != null and dictType != ''">dict_type,</if>
+ <if test="status != null">status,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="dictName != null and dictName != ''">#{dictName},</if>
+ <if test="dictType != null and dictType != ''">#{dictType},</if>
+ <if test="status != null">#{status},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml
new file mode 100644
index 0000000..822d665
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysLogininforMapper">
+
+ <resultMap type="SysLogininfor" id="SysLogininforResult">
+ <id property="infoId" column="info_id" />
+ <result property="userName" column="user_name" />
+ <result property="status" column="status" />
+ <result property="ipaddr" column="ipaddr" />
+ <result property="loginLocation" column="login_location" />
+ <result property="browser" column="browser" />
+ <result property="os" column="os" />
+ <result property="msg" column="msg" />
+ <result property="loginTime" column="login_time" />
+ </resultMap>
+
+ <insert id="insertLogininfor" parameterType="SysLogininfor">
+ insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time)
+ values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate())
+ </insert>
+
+ <select id="selectLogininforList" parameterType="SysLogininfor" resultMap="SysLogininforResult">
+ select info_id, user_name, ipaddr, login_location, browser, os, status, msg, login_time from sys_logininfor
+ <where>
+ <if test="ipaddr != null and ipaddr != ''">
+ AND ipaddr like concat('%', #{ipaddr}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="userName != null and userName != ''">
+ AND user_name like concat('%', #{userName}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ AND login_time >= #{params.beginTime}
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ AND login_time <= #{params.endTime}
+ </if>
+ </where>
+ order by info_id desc
+ </select>
+
+ <delete id="deleteLogininforByIds" parameterType="Long">
+ delete from sys_logininfor where info_id in
+ <foreach collection="array" item="infoId" open="(" separator="," close=")">
+ #{infoId}
+ </foreach>
+ </delete>
+
+ <update id="cleanLogininfor">
+ truncate table sys_logininfor
+ </update>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml
new file mode 100644
index 0000000..84e87c9
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysMenuMapper">
+
+ <resultMap type="SysMenu" id="SysMenuResult">
+ <id property="menuId" column="menu_id" />
+ <result property="menuName" column="menu_name" />
+ <result property="parentName" column="parent_name" />
+ <result property="parentId" column="parent_id" />
+ <result property="orderNum" column="order_num" />
+ <result property="path" column="path" />
+ <result property="component" column="component" />
+ <result property="query" column="query" />
+ <result property="routeName" column="route_name" />
+ <result property="isFrame" column="is_frame" />
+ <result property="isCache" column="is_cache" />
+ <result property="menuType" column="menu_type" />
+ <result property="visible" column="visible" />
+ <result property="status" column="status" />
+ <result property="perms" column="perms" />
+ <result property="icon" column="icon" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateTime" column="update_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectMenuVo">
+ select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
+ from sys_menu
+ </sql>
+
+ <select id="selectMenuList" parameterType="SysMenu" resultMap="SysMenuResult">
+ <include refid="selectMenuVo"/>
+ <where>
+ <if test="menuName != null and menuName != ''">
+ AND menu_name like concat('%', #{menuName}, '%')
+ </if>
+ <if test="visible != null and visible != ''">
+ AND visible = #{visible}
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ </where>
+ order by parent_id, order_num
+ </select>
+
+ <select id="selectMenuTreeAll" resultMap="SysMenuResult">
+ select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+ from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0
+ order by m.parent_id, m.order_num
+ </select>
+
+ <select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
+ select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ left join sys_user_role ur on rm.role_id = ur.role_id
+ left join sys_role ro on ur.role_id = ro.role_id
+ where ur.user_id = #{params.userId}
+ <if test="menuName != null and menuName != ''">
+ AND m.menu_name like concat('%', #{menuName}, '%')
+ </if>
+ <if test="visible != null and visible != ''">
+ AND m.visible = #{visible}
+ </if>
+ <if test="status != null and status != ''">
+ AND m.status = #{status}
+ </if>
+ order by m.parent_id, m.order_num
+ </select>
+
+ <select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
+ select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ left join sys_user_role ur on rm.role_id = ur.role_id
+ left join sys_role ro on ur.role_id = ro.role_id
+ left join sys_user u on ur.user_id = u.user_id
+ where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0
+ order by m.parent_id, m.order_num
+ </select>
+
+ <select id="selectMenuListByRoleId" resultType="Long">
+ select m.menu_id
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ where rm.role_id = #{roleId}
+ <if test="menuCheckStrictly">
+ and m.menu_id not in (select m.parent_id from sys_menu m inner join sys_role_menu rm on m.menu_id = rm.menu_id and rm.role_id = #{roleId})
+ </if>
+ order by m.parent_id, m.order_num
+ </select>
+
+ <select id="selectMenuPerms" resultType="String">
+ select distinct m.perms
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ left join sys_user_role ur on rm.role_id = ur.role_id
+ </select>
+
+ <select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
+ select distinct m.perms
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ left join sys_user_role ur on rm.role_id = ur.role_id
+ left join sys_role r on r.role_id = ur.role_id
+ where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
+ </select>
+
+ <select id="selectMenuPermsByRoleId" parameterType="Long" resultType="String">
+ select distinct m.perms
+ from sys_menu m
+ left join sys_role_menu rm on m.menu_id = rm.menu_id
+ where m.status = '0' and rm.role_id = #{roleId}
+ </select>
+
+ <select id="selectMenuById" parameterType="Long" resultMap="SysMenuResult">
+ <include refid="selectMenuVo"/>
+ where menu_id = #{menuId}
+ </select>
+
+ <select id="hasChildByMenuId" resultType="Integer">
+ select count(1) from sys_menu where parent_id = #{menuId}
+ </select>
+
+ <select id="checkMenuNameUnique" parameterType="SysMenu" resultMap="SysMenuResult">
+ <include refid="selectMenuVo"/>
+ where menu_name=#{menuName} and parent_id = #{parentId} limit 1
+ </select>
+
+ <update id="updateMenu" parameterType="SysMenu">
+ update sys_menu
+ <set>
+ <if test="menuName != null and menuName != ''">menu_name = #{menuName},</if>
+ <if test="parentId != null">parent_id = #{parentId},</if>
+ <if test="orderNum != null">order_num = #{orderNum},</if>
+ <if test="path != null and path != ''">path = #{path},</if>
+ <if test="component != null">component = #{component},</if>
+ <if test="query != null">`query` = #{query},</if>
+ <if test="routeName != null">route_name = #{routeName},</if>
+ <if test="isFrame != null and isFrame != ''">is_frame = #{isFrame},</if>
+ <if test="isCache != null and isCache != ''">is_cache = #{isCache},</if>
+ <if test="menuType != null and menuType != ''">menu_type = #{menuType},</if>
+ <if test="visible != null">visible = #{visible},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="perms !=null">perms = #{perms},</if>
+ <if test="icon !=null and icon != ''">icon = #{icon},</if>
+ <if test="remark != null and remark != ''">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where menu_id = #{menuId}
+ </update>
+
+ <insert id="insertMenu" parameterType="SysMenu">
+ insert into sys_menu(
+ <if test="menuId != null and menuId != 0">menu_id,</if>
+ <if test="parentId != null and parentId != 0">parent_id,</if>
+ <if test="menuName != null and menuName != ''">menu_name,</if>
+ <if test="orderNum != null">order_num,</if>
+ <if test="path != null and path != ''">path,</if>
+ <if test="component != null and component != ''">component,</if>
+ <if test="query != null and query != ''">`query`,</if>
+ <if test="routeName != null">route_name,</if>
+ <if test="isFrame != null and isFrame != ''">is_frame,</if>
+ <if test="isCache != null and isCache != ''">is_cache,</if>
+ <if test="menuType != null and menuType != ''">menu_type,</if>
+ <if test="visible != null">visible,</if>
+ <if test="status != null">status,</if>
+ <if test="perms !=null and perms != ''">perms,</if>
+ <if test="icon != null and icon != ''">icon,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="menuId != null and menuId != 0">#{menuId},</if>
+ <if test="parentId != null and parentId != 0">#{parentId},</if>
+ <if test="menuName != null and menuName != ''">#{menuName},</if>
+ <if test="orderNum != null">#{orderNum},</if>
+ <if test="path != null and path != ''">#{path},</if>
+ <if test="component != null and component != ''">#{component},</if>
+ <if test="query != null and query != ''">#{query},</if>
+ <if test="routeName != null">#{routeName},</if>
+ <if test="isFrame != null and isFrame != ''">#{isFrame},</if>
+ <if test="isCache != null and isCache != ''">#{isCache},</if>
+ <if test="menuType != null and menuType != ''">#{menuType},</if>
+ <if test="visible != null">#{visible},</if>
+ <if test="status != null">#{status},</if>
+ <if test="perms !=null and perms != ''">#{perms},</if>
+ <if test="icon != null and icon != ''">#{icon},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <delete id="deleteMenuById" parameterType="Long">
+ delete from sys_menu where menu_id = #{menuId}
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml
new file mode 100644
index 0000000..65d3079
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysNoticeMapper">
+
+ <resultMap type="SysNotice" id="SysNoticeResult">
+ <result property="noticeId" column="notice_id" />
+ <result property="noticeTitle" column="notice_title" />
+ <result property="noticeType" column="notice_type" />
+ <result property="noticeContent" column="notice_content" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectNoticeVo">
+ select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark
+ from sys_notice
+ </sql>
+
+ <select id="selectNoticeById" parameterType="Long" resultMap="SysNoticeResult">
+ <include refid="selectNoticeVo"/>
+ where notice_id = #{noticeId}
+ </select>
+
+ <select id="selectNoticeList" parameterType="SysNotice" resultMap="SysNoticeResult">
+ <include refid="selectNoticeVo"/>
+ <where>
+ <if test="noticeTitle != null and noticeTitle != ''">
+ AND notice_title like concat('%', #{noticeTitle}, '%')
+ </if>
+ <if test="noticeType != null and noticeType != ''">
+ AND notice_type = #{noticeType}
+ </if>
+ <if test="createBy != null and createBy != ''">
+ AND create_by like concat('%', #{createBy}, '%')
+ </if>
+ </where>
+ </select>
+
+ <insert id="insertNotice" parameterType="SysNotice">
+ insert into sys_notice (
+ <if test="noticeTitle != null and noticeTitle != '' ">notice_title, </if>
+ <if test="noticeType != null and noticeType != '' ">notice_type, </if>
+ <if test="noticeContent != null and noticeContent != '' ">notice_content, </if>
+ <if test="status != null and status != '' ">status, </if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="noticeTitle != null and noticeTitle != ''">#{noticeTitle}, </if>
+ <if test="noticeType != null and noticeType != ''">#{noticeType}, </if>
+ <if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if>
+ <if test="status != null and status != ''">#{status}, </if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateNotice" parameterType="SysNotice">
+ update sys_notice
+ <set>
+ <if test="noticeTitle != null and noticeTitle != ''">notice_title = #{noticeTitle}, </if>
+ <if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if>
+ <if test="noticeContent != null">notice_content = #{noticeContent}, </if>
+ <if test="status != null and status != ''">status = #{status}, </if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where notice_id = #{noticeId}
+ </update>
+
+ <delete id="deleteNoticeById" parameterType="Long">
+ delete from sys_notice where notice_id = #{noticeId}
+ </delete>
+
+ <delete id="deleteNoticeByIds" parameterType="Long">
+ delete from sys_notice where notice_id in
+ <foreach item="noticeId" collection="array" open="(" separator="," close=")">
+ #{noticeId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml
new file mode 100644
index 0000000..201db07
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysOperLogMapper">
+
+ <resultMap type="SysOperLog" id="SysOperLogResult">
+ <id property="operId" column="oper_id" />
+ <result property="title" column="title" />
+ <result property="businessType" column="business_type" />
+ <result property="method" column="method" />
+ <result property="requestMethod" column="request_method" />
+ <result property="operatorType" column="operator_type" />
+ <result property="operName" column="oper_name" />
+ <result property="deptName" column="dept_name" />
+ <result property="operUrl" column="oper_url" />
+ <result property="operIp" column="oper_ip" />
+ <result property="operLocation" column="oper_location" />
+ <result property="operParam" column="oper_param" />
+ <result property="jsonResult" column="json_result" />
+ <result property="status" column="status" />
+ <result property="errorMsg" column="error_msg" />
+ <result property="operTime" column="oper_time" />
+ <result property="costTime" column="cost_time" />
+ </resultMap>
+
+ <sql id="selectOperLogVo">
+ select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time
+ from sys_oper_log
+ </sql>
+
+ <insert id="insertOperlog" parameterType="SysOperLog">
+ insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time)
+ values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate())
+ </insert>
+
+ <select id="selectOperLogList" parameterType="SysOperLog" resultMap="SysOperLogResult">
+ <include refid="selectOperLogVo"/>
+ <where>
+ <if test="operIp != null and operIp != ''">
+ AND oper_ip like concat('%', #{operIp}, '%')
+ </if>
+ <if test="title != null and title != ''">
+ AND title like concat('%', #{title}, '%')
+ </if>
+ <if test="businessType != null">
+ AND business_type = #{businessType}
+ </if>
+ <if test="businessTypes != null and businessTypes.length > 0">
+ AND business_type in
+ <foreach collection="businessTypes" item="businessType" open="(" separator="," close=")">
+ #{businessType}
+ </foreach>
+ </if>
+ <if test="status != null">
+ AND status = #{status}
+ </if>
+ <if test="operName != null and operName != ''">
+ AND oper_name like concat('%', #{operName}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ AND oper_time >= #{params.beginTime}
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ AND oper_time <= #{params.endTime}
+ </if>
+ </where>
+ order by oper_id desc
+ </select>
+
+ <delete id="deleteOperLogByIds" parameterType="Long">
+ delete from sys_oper_log where oper_id in
+ <foreach collection="array" item="operId" open="(" separator="," close=")">
+ #{operId}
+ </foreach>
+ </delete>
+
+ <select id="selectOperLogById" parameterType="Long" resultMap="SysOperLogResult">
+ <include refid="selectOperLogVo"/>
+ where oper_id = #{operId}
+ </select>
+
+ <update id="cleanOperLog">
+ truncate table sys_oper_log
+ </update>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml
new file mode 100644
index 0000000..227c459
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysPostMapper">
+
+ <resultMap type="SysPost" id="SysPostResult">
+ <id property="postId" column="post_id" />
+ <result property="postCode" column="post_code" />
+ <result property="postName" column="post_name" />
+ <result property="postSort" column="post_sort" />
+ <result property="status" column="status" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectPostVo">
+ select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark
+ from sys_post
+ </sql>
+
+ <select id="selectPostList" parameterType="SysPost" resultMap="SysPostResult">
+ <include refid="selectPostVo"/>
+ <where>
+ <if test="postCode != null and postCode != ''">
+ AND post_code like concat('%', #{postCode}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND status = #{status}
+ </if>
+ <if test="postName != null and postName != ''">
+ AND post_name like concat('%', #{postName}, '%')
+ </if>
+ </where>
+ </select>
+
+ <select id="selectPostAll" resultMap="SysPostResult">
+ <include refid="selectPostVo"/>
+ </select>
+
+ <select id="selectPostById" parameterType="Long" resultMap="SysPostResult">
+ <include refid="selectPostVo"/>
+ where post_id = #{postId}
+ </select>
+
+ <select id="selectPostListByUserId" parameterType="Long" resultType="Long">
+ select p.post_id
+ from sys_post p
+ left join sys_user_post up on up.post_id = p.post_id
+ left join sys_user u on u.user_id = up.user_id
+ where u.user_id = #{userId}
+ </select>
+
+ <select id="selectPostsByUserName" parameterType="String" resultMap="SysPostResult">
+ select p.post_id, p.post_name, p.post_code
+ from sys_post p
+ left join sys_user_post up on up.post_id = p.post_id
+ left join sys_user u on u.user_id = up.user_id
+ where u.user_name = #{userName}
+ </select>
+
+ <select id="checkPostNameUnique" parameterType="String" resultMap="SysPostResult">
+ <include refid="selectPostVo"/>
+ where post_name=#{postName} limit 1
+ </select>
+
+ <select id="checkPostCodeUnique" parameterType="String" resultMap="SysPostResult">
+ <include refid="selectPostVo"/>
+ where post_code=#{postCode} limit 1
+ </select>
+
+ <update id="updatePost" parameterType="SysPost">
+ update sys_post
+ <set>
+ <if test="postCode != null and postCode != ''">post_code = #{postCode},</if>
+ <if test="postName != null and postName != ''">post_name = #{postName},</if>
+ <if test="postSort != null">post_sort = #{postSort},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where post_id = #{postId}
+ </update>
+
+ <insert id="insertPost" parameterType="SysPost" useGeneratedKeys="true" keyProperty="postId">
+ insert into sys_post(
+ <if test="postId != null and postId != 0">post_id,</if>
+ <if test="postCode != null and postCode != ''">post_code,</if>
+ <if test="postName != null and postName != ''">post_name,</if>
+ <if test="postSort != null">post_sort,</if>
+ <if test="status != null and status != ''">status,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="postId != null and postId != 0">#{postId},</if>
+ <if test="postCode != null and postCode != ''">#{postCode},</if>
+ <if test="postName != null and postName != ''">#{postName},</if>
+ <if test="postSort != null">#{postSort},</if>
+ <if test="status != null and status != ''">#{status},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <delete id="deletePostById" parameterType="Long">
+ delete from sys_post where post_id = #{postId}
+ </delete>
+
+ <delete id="deletePostByIds" parameterType="Long">
+ delete from sys_post where post_id in
+ <foreach collection="array" item="postId" open="(" separator="," close=")">
+ #{postId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml
new file mode 100644
index 0000000..7c4139b
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysRoleDeptMapper">
+
+ <resultMap type="SysRoleDept" id="SysRoleDeptResult">
+ <result property="roleId" column="role_id" />
+ <result property="deptId" column="dept_id" />
+ </resultMap>
+
+ <delete id="deleteRoleDeptByRoleId" parameterType="Long">
+ delete from sys_role_dept where role_id=#{roleId}
+ </delete>
+
+ <select id="selectCountRoleDeptByDeptId" resultType="Integer">
+ select count(1) from sys_role_dept where dept_id=#{deptId}
+ </select>
+
+ <delete id="deleteRoleDept" parameterType="Long">
+ delete from sys_role_dept where role_id in
+ <foreach collection="array" item="roleId" open="(" separator="," close=")">
+ #{roleId}
+ </foreach>
+ </delete>
+
+ <insert id="batchRoleDept">
+ insert into sys_role_dept(role_id, dept_id) values
+ <foreach item="item" index="index" collection="list" separator=",">
+ (#{item.roleId},#{item.deptId})
+ </foreach>
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml
new file mode 100644
index 0000000..955d4ee
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysRoleMapper">
+
+ <resultMap type="SysRole" id="SysRoleResult">
+ <id property="roleId" column="role_id" />
+ <result property="roleName" column="role_name" />
+ <result property="roleKey" column="role_key" />
+ <result property="roleSort" column="role_sort" />
+ <result property="dataScope" column="data_scope" />
+ <result property="menuCheckStrictly" column="menu_check_strictly" />
+ <result property="deptCheckStrictly" column="dept_check_strictly" />
+ <result property="status" column="status" />
+ <result property="delFlag" column="del_flag" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectRoleVo">
+ select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly,
+ r.status, r.del_flag, r.create_time, r.remark
+ from sys_role r
+ left join sys_user_role ur on ur.role_id = r.role_id
+ left join sys_user u on u.user_id = ur.user_id
+ left join sys_dept d on u.dept_id = d.dept_id
+ </sql>
+
+ <select id="selectRoleList" parameterType="SysRole" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ where r.del_flag = '0'
+ <if test="roleId != null and roleId != 0">
+ AND r.role_id = #{roleId}
+ </if>
+ <if test="roleName != null and roleName != ''">
+ AND r.role_name like concat('%', #{roleName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND r.status = #{status}
+ </if>
+ <if test="roleKey != null and roleKey != ''">
+ AND r.role_key like concat('%', #{roleKey}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ and date_format(r.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ and date_format(r.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ <!-- 数据范围过滤 -->
+ ${params.dataScope}
+ order by r.role_sort
+ </select>
+
+ <select id="selectRolePermissionByUserId" parameterType="Long" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ WHERE r.del_flag = '0' and ur.user_id = #{userId}
+ </select>
+
+ <select id="selectRoleAll" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ </select>
+
+ <select id="selectRoleListByUserId" parameterType="Long" resultType="Long">
+ select r.role_id
+ from sys_role r
+ left join sys_user_role ur on ur.role_id = r.role_id
+ left join sys_user u on u.user_id = ur.user_id
+ where u.user_id = #{userId}
+ </select>
+
+ <select id="selectRoleById" parameterType="Long" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ where r.role_id = #{roleId}
+ </select>
+
+ <select id="selectRolesByUserName" parameterType="String" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ WHERE r.del_flag = '0' and u.user_name = #{userName}
+ </select>
+
+ <select id="checkRoleNameUnique" parameterType="String" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ where r.role_name=#{roleName} and r.del_flag = '0' limit 1
+ </select>
+
+ <select id="checkRoleKeyUnique" parameterType="String" resultMap="SysRoleResult">
+ <include refid="selectRoleVo"/>
+ where r.role_key=#{roleKey} and r.del_flag = '0' limit 1
+ </select>
+
+ <insert id="insertRole" parameterType="SysRole" useGeneratedKeys="true" keyProperty="roleId">
+ insert into sys_role(
+ <if test="roleId != null and roleId != 0">role_id,</if>
+ <if test="roleName != null and roleName != ''">role_name,</if>
+ <if test="roleKey != null and roleKey != ''">role_key,</if>
+ <if test="roleSort != null">role_sort,</if>
+ <if test="dataScope != null and dataScope != ''">data_scope,</if>
+ <if test="menuCheckStrictly != null">menu_check_strictly,</if>
+ <if test="deptCheckStrictly != null">dept_check_strictly,</if>
+ <if test="status != null and status != ''">status,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ create_time
+ )values(
+ <if test="roleId != null and roleId != 0">#{roleId},</if>
+ <if test="roleName != null and roleName != ''">#{roleName},</if>
+ <if test="roleKey != null and roleKey != ''">#{roleKey},</if>
+ <if test="roleSort != null">#{roleSort},</if>
+ <if test="dataScope != null and dataScope != ''">#{dataScope},</if>
+ <if test="menuCheckStrictly != null">#{menuCheckStrictly},</if>
+ <if test="deptCheckStrictly != null">#{deptCheckStrictly},</if>
+ <if test="status != null and status != ''">#{status},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateRole" parameterType="SysRole">
+ update sys_role
+ <set>
+ <if test="roleName != null and roleName != ''">role_name = #{roleName},</if>
+ <if test="roleKey != null and roleKey != ''">role_key = #{roleKey},</if>
+ <if test="roleSort != null">role_sort = #{roleSort},</if>
+ <if test="dataScope != null and dataScope != ''">data_scope = #{dataScope},</if>
+ <if test="menuCheckStrictly != null">menu_check_strictly = #{menuCheckStrictly},</if>
+ <if test="deptCheckStrictly != null">dept_check_strictly = #{deptCheckStrictly},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ update_time = sysdate()
+ </set>
+ where role_id = #{roleId}
+ </update>
+
+ <delete id="deleteRoleById" parameterType="Long">
+ update sys_role set del_flag = '2' where role_id = #{roleId}
+ </delete>
+
+ <delete id="deleteRoleByIds" parameterType="Long">
+ update sys_role set del_flag = '2' where role_id in
+ <foreach collection="array" item="roleId" open="(" separator="," close=")">
+ #{roleId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml
new file mode 100644
index 0000000..cb60a85
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysRoleMenuMapper">
+
+ <resultMap type="SysRoleMenu" id="SysRoleMenuResult">
+ <result property="roleId" column="role_id" />
+ <result property="menuId" column="menu_id" />
+ </resultMap>
+
+ <select id="checkMenuExistRole" resultType="Integer">
+ select count(1) from sys_role_menu where menu_id = #{menuId}
+ </select>
+
+ <delete id="deleteRoleMenuByRoleId" parameterType="Long">
+ delete from sys_role_menu where role_id=#{roleId}
+ </delete>
+
+ <delete id="deleteRoleMenu" parameterType="Long">
+ delete from sys_role_menu where role_id in
+ <foreach collection="array" item="roleId" open="(" separator="," close=")">
+ #{roleId}
+ </foreach>
+ </delete>
+
+ <insert id="batchRoleMenu">
+ insert into sys_role_menu(role_id, menu_id) values
+ <foreach item="item" index="index" collection="list" separator=",">
+ (#{item.roleId},#{item.menuId})
+ </foreach>
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
new file mode 100644
index 0000000..0856cb2
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysUserMapper">
+
+ <resultMap type="SysUser" id="SysUserResult">
+ <id property="userId" column="user_id" />
+ <result property="deptId" column="dept_id" />
+ <result property="userName" column="user_name" />
+ <result property="nickName" column="nick_name" />
+ <result property="email" column="email" />
+ <result property="phonenumber" column="phonenumber" />
+ <result property="sex" column="sex" />
+ <result property="avatar" column="avatar" />
+ <result property="password" column="password" />
+ <result property="status" column="status" />
+ <result property="delFlag" column="del_flag" />
+ <result property="loginIp" column="login_ip" />
+ <result property="loginDate" column="login_date" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ <association property="dept" javaType="SysDept" resultMap="deptResult" />
+ <collection property="roles" javaType="java.util.List" resultMap="RoleResult" />
+ </resultMap>
+
+ <resultMap id="deptResult" type="SysDept">
+ <id property="deptId" column="dept_id" />
+ <result property="parentId" column="parent_id" />
+ <result property="deptName" column="dept_name" />
+ <result property="ancestors" column="ancestors" />
+ <result property="orderNum" column="order_num" />
+ <result property="leader" column="leader" />
+ <result property="status" column="dept_status" />
+ </resultMap>
+
+ <resultMap id="RoleResult" type="SysRole">
+ <id property="roleId" column="role_id" />
+ <result property="roleName" column="role_name" />
+ <result property="roleKey" column="role_key" />
+ <result property="roleSort" column="role_sort" />
+ <result property="dataScope" column="data_scope" />
+ <result property="status" column="role_status" />
+ </resultMap>
+
+ <sql id="selectUserVo">
+ select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
+ d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
+ r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
+ from sys_user u
+ left join sys_dept d on u.dept_id = d.dept_id
+ left join sys_user_role ur on u.user_id = ur.user_id
+ left join sys_role r on r.role_id = ur.role_id
+ </sql>
+
+ <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
+ select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
+ left join sys_dept d on u.dept_id = d.dept_id
+ where u.del_flag = '0'
+ <if test="userId != null and userId != 0">
+ AND u.user_id = #{userId}
+ </if>
+ <if test="userName != null and userName != ''">
+ AND u.user_name like concat('%', #{userName}, '%')
+ </if>
+ <if test="status != null and status != ''">
+ AND u.status = #{status}
+ </if>
+ <if test="phonenumber != null and phonenumber != ''">
+ AND u.phonenumber like concat('%', #{phonenumber}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+ AND date_format(u.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+ AND date_format(u.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
+ </if>
+ <if test="deptId != null and deptId != 0">
+ AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
+ </if>
+ <!-- 数据范围过滤 -->
+ ${params.dataScope}
+ </select>
+
+ <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">
+ select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
+ from sys_user u
+ left join sys_dept d on u.dept_id = d.dept_id
+ left join sys_user_role ur on u.user_id = ur.user_id
+ left join sys_role r on r.role_id = ur.role_id
+ where u.del_flag = '0' and r.role_id = #{roleId}
+ <if test="userName != null and userName != ''">
+ AND u.user_name like concat('%', #{userName}, '%')
+ </if>
+ <if test="phonenumber != null and phonenumber != ''">
+ AND u.phonenumber like concat('%', #{phonenumber}, '%')
+ </if>
+ <!-- 数据范围过滤 -->
+ ${params.dataScope}
+ </select>
+
+ <select id="selectUnallocatedList" parameterType="SysUser" resultMap="SysUserResult">
+ select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
+ from sys_user u
+ left join sys_dept d on u.dept_id = d.dept_id
+ left join sys_user_role ur on u.user_id = ur.user_id
+ left join sys_role r on r.role_id = ur.role_id
+ where u.del_flag = '0' and (r.role_id != #{roleId} or r.role_id IS NULL)
+ and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and ur.role_id = #{roleId})
+ <if test="userName != null and userName != ''">
+ AND u.user_name like concat('%', #{userName}, '%')
+ </if>
+ <if test="phonenumber != null and phonenumber != ''">
+ AND u.phonenumber like concat('%', #{phonenumber}, '%')
+ </if>
+ <!-- 数据范围过滤 -->
+ ${params.dataScope}
+ </select>
+
+ <select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
+ <include refid="selectUserVo"/>
+ where u.user_name = #{userName} and u.del_flag = '0'
+ </select>
+
+ <select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
+ <include refid="selectUserVo"/>
+ where u.user_id = #{userId}
+ </select>
+
+ <select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
+ select user_id, user_name from sys_user where user_name = #{userName} and del_flag = '0' limit 1
+ </select>
+
+ <select id="checkPhoneUnique" parameterType="String" resultMap="SysUserResult">
+ select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and del_flag = '0' limit 1
+ </select>
+
+ <select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
+ select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
+ </select>
+
+ <insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
+ insert into sys_user(
+ <if test="userId != null and userId != 0">user_id,</if>
+ <if test="deptId != null and deptId != 0">dept_id,</if>
+ <if test="userName != null and userName != ''">user_name,</if>
+ <if test="nickName != null and nickName != ''">nick_name,</if>
+ <if test="email != null and email != ''">email,</if>
+ <if test="avatar != null and avatar != ''">avatar,</if>
+ <if test="phonenumber != null and phonenumber != ''">phonenumber,</if>
+ <if test="sex != null and sex != ''">sex,</if>
+ <if test="password != null and password != ''">password,</if>
+ <if test="status != null and status != ''">status,</if>
+ <if test="createBy != null and createBy != ''">create_by,</if>
+ <if test="remark != null and remark != ''">remark,</if>
+ create_time
+ )values(
+ <if test="userId != null and userId != ''">#{userId},</if>
+ <if test="deptId != null and deptId != ''">#{deptId},</if>
+ <if test="userName != null and userName != ''">#{userName},</if>
+ <if test="nickName != null and nickName != ''">#{nickName},</if>
+ <if test="email != null and email != ''">#{email},</if>
+ <if test="avatar != null and avatar != ''">#{avatar},</if>
+ <if test="phonenumber != null and phonenumber != ''">#{phonenumber},</if>
+ <if test="sex != null and sex != ''">#{sex},</if>
+ <if test="password != null and password != ''">#{password},</if>
+ <if test="status != null and status != ''">#{status},</if>
+ <if test="createBy != null and createBy != ''">#{createBy},</if>
+ <if test="remark != null and remark != ''">#{remark},</if>
+ sysdate()
+ )
+ </insert>
+
+ <update id="updateUser" parameterType="SysUser">
+ update sys_user
+ <set>
+ <if test="deptId != null and deptId != 0">dept_id = #{deptId},</if>
+ <if test="userName != null and userName != ''">user_name = #{userName},</if>
+ <if test="nickName != null and nickName != ''">nick_name = #{nickName},</if>
+ <if test="email != null ">email = #{email},</if>
+ <if test="phonenumber != null ">phonenumber = #{phonenumber},</if>
+ <if test="sex != null and sex != ''">sex = #{sex},</if>
+ <if test="avatar != null and avatar != ''">avatar = #{avatar},</if>
+ <if test="password != null and password != ''">password = #{password},</if>
+ <if test="status != null and status != ''">status = #{status},</if>
+ <if test="loginIp != null and loginIp != ''">login_ip = #{loginIp},</if>
+ <if test="loginDate != null">login_date = #{loginDate},</if>
+ <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ update_time = sysdate()
+ </set>
+ where user_id = #{userId}
+ </update>
+
+ <update id="updateUserStatus" parameterType="SysUser">
+ update sys_user set status = #{status} where user_id = #{userId}
+ </update>
+
+ <update id="updateUserAvatar" parameterType="SysUser">
+ update sys_user set avatar = #{avatar} where user_name = #{userName}
+ </update>
+
+ <update id="resetUserPwd" parameterType="SysUser">
+ update sys_user set password = #{password} where user_name = #{userName}
+ </update>
+
+ <delete id="deleteUserById" parameterType="Long">
+ update sys_user set del_flag = '2' where user_id = #{userId}
+ </delete>
+
+ <delete id="deleteUserByIds" parameterType="Long">
+ update sys_user set del_flag = '2' where user_id in
+ <foreach collection="array" item="userId" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml
new file mode 100644
index 0000000..2b90bc4
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysUserPostMapper">
+
+ <resultMap type="SysUserPost" id="SysUserPostResult">
+ <result property="userId" column="user_id" />
+ <result property="postId" column="post_id" />
+ </resultMap>
+
+ <delete id="deleteUserPostByUserId" parameterType="Long">
+ delete from sys_user_post where user_id=#{userId}
+ </delete>
+
+ <select id="countUserPostById" resultType="Integer">
+ select count(1) from sys_user_post where post_id=#{postId}
+ </select>
+
+ <delete id="deleteUserPost" parameterType="Long">
+ delete from sys_user_post where user_id in
+ <foreach collection="array" item="userId" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+
+ <insert id="batchUserPost">
+ insert into sys_user_post(user_id, post_id) values
+ <foreach item="item" index="index" collection="list" separator=",">
+ (#{item.userId},#{item.postId})
+ </foreach>
+ </insert>
+
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml
new file mode 100644
index 0000000..dd72689
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysUserRoleMapper">
+
+ <resultMap type="SysUserRole" id="SysUserRoleResult">
+ <result property="userId" column="user_id" />
+ <result property="roleId" column="role_id" />
+ </resultMap>
+
+ <delete id="deleteUserRoleByUserId" parameterType="Long">
+ delete from sys_user_role where user_id=#{userId}
+ </delete>
+
+ <select id="countUserRoleByRoleId" resultType="Integer">
+ select count(1) from sys_user_role where role_id=#{roleId}
+ </select>
+
+ <delete id="deleteUserRole" parameterType="Long">
+ delete from sys_user_role where user_id in
+ <foreach collection="array" item="userId" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+
+ <insert id="batchUserRole">
+ insert into sys_user_role(user_id, role_id) values
+ <foreach item="item" index="index" collection="list" separator=",">
+ (#{item.userId},#{item.roleId})
+ </foreach>
+ </insert>
+
+ <delete id="deleteUserRoleInfo" parameterType="SysUserRole">
+ delete from sys_user_role where user_id=#{userId} and role_id=#{roleId}
+ </delete>
+
+ <delete id="deleteUserRoleInfos">
+ delete from sys_user_role where role_id=#{roleId} and user_id in
+ <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+</mapper>
\ No newline at end of file
diff --git a/sql/quartz.sql b/sql/quartz.sql
new file mode 100644
index 0000000..cee613b
--- /dev/null
+++ b/sql/quartz.sql
@@ -0,0 +1,174 @@
+DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
+DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
+DROP TABLE IF EXISTS QRTZ_LOCKS;
+DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_TRIGGERS;
+DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
+DROP TABLE IF EXISTS QRTZ_CALENDARS;
+
+-- ----------------------------
+-- 1、存储每一个已配置的 jobDetail 的详细信息
+-- ----------------------------
+create table QRTZ_JOB_DETAILS (
+ sched_name varchar(120) not null comment '调度名称',
+ job_name varchar(200) not null comment '任务名称',
+ job_group varchar(200) not null comment '任务组名',
+ description varchar(250) null comment '相关介绍',
+ job_class_name varchar(250) not null comment '执行任务类名称',
+ is_durable varchar(1) not null comment '是否持久化',
+ is_nonconcurrent varchar(1) not null comment '是否并发',
+ is_update_data varchar(1) not null comment '是否更新数据',
+ requests_recovery varchar(1) not null comment '是否接受恢复执行',
+ job_data blob null comment '存放持久化job对象',
+ primary key (sched_name, job_name, job_group)
+) engine=innodb comment = '任务详细信息表';
+
+-- ----------------------------
+-- 2、 存储已配置的 Trigger 的信息
+-- ----------------------------
+create table QRTZ_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_name varchar(200) not null comment '触发器的名字',
+ trigger_group varchar(200) not null comment '触发器所属组的名字',
+ job_name varchar(200) not null comment 'qrtz_job_details表job_name的外键',
+ job_group varchar(200) not null comment 'qrtz_job_details表job_group的外键',
+ description varchar(250) null comment '相关介绍',
+ next_fire_time bigint(13) null comment '上一次触发时间(毫秒)',
+ prev_fire_time bigint(13) null comment '下一次触发时间(默认为-1表示不触发)',
+ priority integer null comment '优先级',
+ trigger_state varchar(16) not null comment '触发器状态',
+ trigger_type varchar(8) not null comment '触发器的类型',
+ start_time bigint(13) not null comment '开始时间',
+ end_time bigint(13) null comment '结束时间',
+ calendar_name varchar(200) null comment '日程表名称',
+ misfire_instr smallint(2) null comment '补偿执行的策略',
+ job_data blob null comment '存放持久化job对象',
+ primary key (sched_name, trigger_name, trigger_group),
+ foreign key (sched_name, job_name, job_group) references QRTZ_JOB_DETAILS(sched_name, job_name, job_group)
+) engine=innodb comment = '触发器详细信息表';
+
+-- ----------------------------
+-- 3、 存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数
+-- ----------------------------
+create table QRTZ_SIMPLE_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ repeat_count bigint(7) not null comment '重复的次数统计',
+ repeat_interval bigint(12) not null comment '重复的间隔时间',
+ times_triggered bigint(10) not null comment '已经触发的次数',
+ primary key (sched_name, trigger_name, trigger_group),
+ foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
+) engine=innodb comment = '简单触发器的信息表';
+
+-- ----------------------------
+-- 4、 存储 Cron Trigger,包括 Cron 表达式和时区信息
+-- ----------------------------
+create table QRTZ_CRON_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ cron_expression varchar(200) not null comment 'cron表达式',
+ time_zone_id varchar(80) comment '时区',
+ primary key (sched_name, trigger_name, trigger_group),
+ foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
+) engine=innodb comment = 'Cron类型的触发器表';
+
+-- ----------------------------
+-- 5、 Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)
+-- ----------------------------
+create table QRTZ_BLOB_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ blob_data blob null comment '存放持久化Trigger对象',
+ primary key (sched_name, trigger_name, trigger_group),
+ foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
+) engine=innodb comment = 'Blob类型的触发器表';
+
+-- ----------------------------
+-- 6、 以 Blob 类型存储存放日历信息, quartz可配置一个日历来指定一个时间范围
+-- ----------------------------
+create table QRTZ_CALENDARS (
+ sched_name varchar(120) not null comment '调度名称',
+ calendar_name varchar(200) not null comment '日历名称',
+ calendar blob not null comment '存放持久化calendar对象',
+ primary key (sched_name, calendar_name)
+) engine=innodb comment = '日历信息表';
+
+-- ----------------------------
+-- 7、 存储已暂停的 Trigger 组的信息
+-- ----------------------------
+create table QRTZ_PAUSED_TRIGGER_GRPS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ primary key (sched_name, trigger_group)
+) engine=innodb comment = '暂停的触发器表';
+
+-- ----------------------------
+-- 8、 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息
+-- ----------------------------
+create table QRTZ_FIRED_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ entry_id varchar(95) not null comment '调度器实例id',
+ trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ instance_name varchar(200) not null comment '调度器实例名',
+ fired_time bigint(13) not null comment '触发的时间',
+ sched_time bigint(13) not null comment '定时器制定的时间',
+ priority integer not null comment '优先级',
+ state varchar(16) not null comment '状态',
+ job_name varchar(200) null comment '任务名称',
+ job_group varchar(200) null comment '任务组名',
+ is_nonconcurrent varchar(1) null comment '是否并发',
+ requests_recovery varchar(1) null comment '是否接受恢复执行',
+ primary key (sched_name, entry_id)
+) engine=innodb comment = '已触发的触发器表';
+
+-- ----------------------------
+-- 9、 存储少量的有关 Scheduler 的状态信息,假如是用于集群中,可以看到其他的 Scheduler 实例
+-- ----------------------------
+create table QRTZ_SCHEDULER_STATE (
+ sched_name varchar(120) not null comment '调度名称',
+ instance_name varchar(200) not null comment '实例名称',
+ last_checkin_time bigint(13) not null comment '上次检查时间',
+ checkin_interval bigint(13) not null comment '检查间隔时间',
+ primary key (sched_name, instance_name)
+) engine=innodb comment = '调度器状态表';
+
+-- ----------------------------
+-- 10、 存储程序的悲观锁的信息(假如使用了悲观锁)
+-- ----------------------------
+create table QRTZ_LOCKS (
+ sched_name varchar(120) not null comment '调度名称',
+ lock_name varchar(40) not null comment '悲观锁名称',
+ primary key (sched_name, lock_name)
+) engine=innodb comment = '存储的悲观锁信息表';
+
+-- ----------------------------
+-- 11、 Quartz集群实现同步机制的行锁表
+-- ----------------------------
+create table QRTZ_SIMPROP_TRIGGERS (
+ sched_name varchar(120) not null comment '调度名称',
+ trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键',
+ trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键',
+ str_prop_1 varchar(512) null comment 'String类型的trigger的第一个参数',
+ str_prop_2 varchar(512) null comment 'String类型的trigger的第二个参数',
+ str_prop_3 varchar(512) null comment 'String类型的trigger的第三个参数',
+ int_prop_1 int null comment 'int类型的trigger的第一个参数',
+ int_prop_2 int null comment 'int类型的trigger的第二个参数',
+ long_prop_1 bigint null comment 'long类型的trigger的第一个参数',
+ long_prop_2 bigint null comment 'long类型的trigger的第二个参数',
+ dec_prop_1 numeric(13,4) null comment 'decimal类型的trigger的第一个参数',
+ dec_prop_2 numeric(13,4) null comment 'decimal类型的trigger的第二个参数',
+ bool_prop_1 varchar(1) null comment 'Boolean类型的trigger的第一个参数',
+ bool_prop_2 varchar(1) null comment 'Boolean类型的trigger的第二个参数',
+ primary key (sched_name, trigger_name, trigger_group),
+ foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
+) engine=innodb comment = '同步机制的行锁表';
+
+commit;
\ No newline at end of file
diff --git a/sql/ry_react.sql b/sql/ry_react.sql
new file mode 100644
index 0000000..1f39f31
--- /dev/null
+++ b/sql/ry_react.sql
@@ -0,0 +1,705 @@
+-- ----------------------------
+-- 1、部门表
+-- ----------------------------
+drop table if exists sys_dept;
+create table sys_dept (
+ dept_id bigint(20) not null auto_increment comment '部门id',
+ parent_id bigint(20) default 0 comment '父部门id',
+ ancestors varchar(50) default '' comment '祖级列表',
+ dept_name varchar(30) default '' comment '部门名称',
+ order_num int(4) default 0 comment '显示顺序',
+ leader varchar(20) default null comment '负责人',
+ phone varchar(11) default null comment '联系电话',
+ email varchar(50) default null comment '邮箱',
+ status char(1) default '0' comment '部门状态(0正常 1停用)',
+ del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ primary key (dept_id)
+) engine=innodb auto_increment=200 comment = '部门表';
+
+-- ----------------------------
+-- 初始化-部门表数据
+-- ----------------------------
+insert into sys_dept values(100, 0, '0', '若依科技', 0, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(101, 100, '0,100', '深圳总公司', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(102, 100, '0,100', '长沙分公司', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
+
+
+-- ----------------------------
+-- 2、用户信息表
+-- ----------------------------
+drop table if exists sys_user;
+create table sys_user (
+ user_id bigint(20) not null auto_increment comment '用户ID',
+ dept_id bigint(20) default null comment '部门ID',
+ user_name varchar(30) not null comment '用户账号',
+ nick_name varchar(30) not null comment '用户昵称',
+ user_type varchar(2) default '00' comment '用户类型(00系统用户)',
+ email varchar(50) default '' comment '用户邮箱',
+ phonenumber varchar(11) default '' comment '手机号码',
+ sex char(1) default '0' comment '用户性别(0男 1女 2未知)',
+ avatar varchar(100) default '' comment '头像地址',
+ password varchar(100) default '' comment '密码',
+ status char(1) default '0' comment '帐号状态(0正常 1停用)',
+ del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
+ login_ip varchar(128) default '' comment '最后登录IP',
+ login_date datetime comment '最后登录时间',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (user_id)
+) engine=innodb auto_increment=100 comment = '用户信息表';
+
+-- ----------------------------
+-- 初始化-用户信息表数据
+-- ----------------------------
+insert into sys_user values(1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '管理员');
+insert into sys_user values(2, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '测试员');
+
+
+-- ----------------------------
+-- 3、岗位信息表
+-- ----------------------------
+drop table if exists sys_post;
+create table sys_post
+(
+ post_id bigint(20) not null auto_increment comment '岗位ID',
+ post_code varchar(64) not null comment '岗位编码',
+ post_name varchar(50) not null comment '岗位名称',
+ post_sort int(4) not null comment '显示顺序',
+ status char(1) not null comment '状态(0正常 1停用)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (post_id)
+) engine=innodb comment = '岗位信息表';
+
+-- ----------------------------
+-- 初始化-岗位信息表数据
+-- ----------------------------
+insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, '');
+insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, '');
+insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, '');
+insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, '');
+
+
+-- ----------------------------
+-- 4、角色信息表
+-- ----------------------------
+drop table if exists sys_role;
+create table sys_role (
+ role_id bigint(20) not null auto_increment comment '角色ID',
+ role_name varchar(30) not null comment '角色名称',
+ role_key varchar(100) not null comment '角色权限字符串',
+ role_sort int(4) not null comment '显示顺序',
+ data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
+ menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示',
+ dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示',
+ status char(1) not null comment '角色状态(0正常 1停用)',
+ del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (role_id)
+) engine=innodb auto_increment=100 comment = '角色信息表';
+
+-- ----------------------------
+-- 初始化-角色信息表数据
+-- ----------------------------
+insert into sys_role values('1', '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员');
+insert into sys_role values('2', '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色');
+
+
+-- ----------------------------
+-- 5、菜单权限表
+-- ----------------------------
+drop table if exists sys_menu;
+create table sys_menu (
+ menu_id bigint(20) not null auto_increment comment '菜单ID',
+ menu_name varchar(50) not null comment '菜单名称',
+ parent_id bigint(20) default 0 comment '父菜单ID',
+ order_num int(4) default 0 comment '显示顺序',
+ path varchar(200) default '' comment '路由地址',
+ component varchar(255) default null comment '组件路径',
+ query varchar(255) default null comment '路由参数',
+ route_name varchar(50) default '' comment '路由名称',
+ is_frame int(1) default 1 comment '是否为外链(0是 1否)',
+ is_cache int(1) default 0 comment '是否缓存(0缓存 1不缓存)',
+ menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)',
+ visible char(1) default 0 comment '菜单状态(0显示 1隐藏)',
+ status char(1) default 0 comment '菜单状态(0正常 1停用)',
+ perms varchar(100) default null comment '权限标识',
+ icon varchar(100) default '#' comment '菜单图标',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default '' comment '备注',
+ primary key (menu_id)
+) engine=innodb auto_increment=2000 comment = '菜单权限表';
+
+-- ----------------------------
+-- 初始化-菜单信息表数据
+-- ----------------------------
+-- 一级菜单
+insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'SettingOutlined', 'admin', sysdate(), '', null, '系统管理目录');
+insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'DashboardOutlined', 'admin', sysdate(), '', null, '系统监控目录');
+insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'ToolOutlined', 'admin', sysdate(), '', null, '系统工具目录');
+insert into sys_menu values('4', '若依官网', '0', '4', 'http://ruoyi.vip', null, '', '', 0, 0, 'M', '0', '0', '', 'LinkOutlined', 'admin', sysdate(), '', null, '若依官网地址');
+
+insert into sys_menu values('5', '控制台', '0', '0', 'dashboard', null, '', '', 1, 0, 'M', '1', '0', '', 'AppstoreOutlined', 'admin', sysdate(), '', null, '控制台');
+insert into sys_menu values('6', '个人', '0', '0', 'account', null, '', '', 1, 0, 'M', '1', '0', '', 'ProfileOutlined', 'admin', sysdate(), '', null, '个人');
+
+-- 二级菜单
+insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单');
+insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单');
+insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单');
+insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单');
+insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单');
+insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单');
+insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单');
+insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单');
+insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单');
+insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单');
+insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单');
+insert into sys_menu values('111', '数据监控', '2', '3', 'druid', 'monitor/druid/index', '', '', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', sysdate(), '', null, '数据监控菜单');
+insert into sys_menu values('112', '服务监控', '2', '4', 'server', 'monitor/server/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单');
+insert into sys_menu values('113', '缓存监控', '2', '5', 'cache', 'monitor/cache/index', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', sysdate(), '', null, '缓存监控菜单');
+insert into sys_menu values('114', '缓存列表', '2', '6', 'cacheList', 'monitor/cache/list', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis-list', 'admin', sysdate(), '', null, '缓存列表菜单');
+insert into sys_menu values('115', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单');
+insert into sys_menu values('116', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单');
+insert into sys_menu values('117', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单');
+-- 三级菜单
+insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单');
+insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单');
+-- 用户管理按钮
+insert into sys_menu values('1000', '用户查询', '100', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1001', '用户新增', '100', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1002', '用户修改', '100', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1003', '用户删除', '100', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1004', '用户导出', '100', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1005', '用户导入', '100', '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1006', '重置密码', '100', '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, '');
+-- 角色管理按钮
+insert into sys_menu values('1007', '角色查询', '101', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1008', '角色新增', '101', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1009', '角色修改', '101', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1010', '角色删除', '101', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1011', '角色导出', '101', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, '');
+-- 菜单管理按钮
+insert into sys_menu values('1012', '菜单查询', '102', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1013', '菜单新增', '102', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1014', '菜单修改', '102', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1015', '菜单删除', '102', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, '');
+-- 部门管理按钮
+insert into sys_menu values('1016', '部门查询', '103', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1017', '部门新增', '103', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1018', '部门修改', '103', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1019', '部门删除', '103', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, '');
+-- 岗位管理按钮
+insert into sys_menu values('1020', '岗位查询', '104', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1021', '岗位新增', '104', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1022', '岗位修改', '104', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1023', '岗位删除', '104', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1024', '岗位导出', '104', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, '');
+-- 字典管理按钮
+insert into sys_menu values('1025', '字典查询', '105', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1026', '字典新增', '105', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1027', '字典修改', '105', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1028', '字典删除', '105', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1029', '字典导出', '105', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, '');
+-- 参数设置按钮
+insert into sys_menu values('1030', '参数查询', '106', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1031', '参数新增', '106', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1032', '参数修改', '106', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1033', '参数删除', '106', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1034', '参数导出', '106', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, '');
+-- 通知公告按钮
+insert into sys_menu values('1035', '公告查询', '107', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1036', '公告新增', '107', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1037', '公告修改', '107', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1038', '公告删除', '107', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, '');
+-- 操作日志按钮
+insert into sys_menu values('1039', '操作查询', '500', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1040', '操作删除', '500', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1041', '日志导出', '500', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', sysdate(), '', null, '');
+-- 登录日志按钮
+insert into sys_menu values('1042', '登录查询', '501', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1043', '登录删除', '501', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1044', '日志导出', '501', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1045', '账户解锁', '501', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 'admin', sysdate(), '', null, '');
+-- 在线用户按钮
+insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, '');
+-- 定时任务按钮
+insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1054', '任务导出', '110', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, '');
+-- 代码生成按钮
+insert into sys_menu values('1055', '生成查询', '116', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1056', '生成修改', '116', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1057', '生成删除', '116', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, '');
+insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, '');
+
+
+-- ----------------------------
+-- 6、用户和角色关联表 用户N-1角色
+-- ----------------------------
+drop table if exists sys_user_role;
+create table sys_user_role (
+ user_id bigint(20) not null comment '用户ID',
+ role_id bigint(20) not null comment '角色ID',
+ primary key(user_id, role_id)
+) engine=innodb comment = '用户和角色关联表';
+
+-- ----------------------------
+-- 初始化-用户和角色关联表数据
+-- ----------------------------
+insert into sys_user_role values ('1', '1');
+insert into sys_user_role values ('2', '2');
+
+
+-- ----------------------------
+-- 7、角色和菜单关联表 角色1-N菜单
+-- ----------------------------
+drop table if exists sys_role_menu;
+create table sys_role_menu (
+ role_id bigint(20) not null comment '角色ID',
+ menu_id bigint(20) not null comment '菜单ID',
+ primary key(role_id, menu_id)
+) engine=innodb comment = '角色和菜单关联表';
+
+-- ----------------------------
+-- 初始化-角色和菜单关联表数据
+-- ----------------------------
+insert into sys_role_menu values ('2', '1');
+insert into sys_role_menu values ('2', '2');
+insert into sys_role_menu values ('2', '3');
+insert into sys_role_menu values ('2', '4');
+insert into sys_role_menu values ('2', '100');
+insert into sys_role_menu values ('2', '101');
+insert into sys_role_menu values ('2', '102');
+insert into sys_role_menu values ('2', '103');
+insert into sys_role_menu values ('2', '104');
+insert into sys_role_menu values ('2', '105');
+insert into sys_role_menu values ('2', '106');
+insert into sys_role_menu values ('2', '107');
+insert into sys_role_menu values ('2', '108');
+insert into sys_role_menu values ('2', '109');
+insert into sys_role_menu values ('2', '110');
+insert into sys_role_menu values ('2', '111');
+insert into sys_role_menu values ('2', '112');
+insert into sys_role_menu values ('2', '113');
+insert into sys_role_menu values ('2', '114');
+insert into sys_role_menu values ('2', '115');
+insert into sys_role_menu values ('2', '116');
+insert into sys_role_menu values ('2', '117');
+insert into sys_role_menu values ('2', '500');
+insert into sys_role_menu values ('2', '501');
+insert into sys_role_menu values ('2', '1000');
+insert into sys_role_menu values ('2', '1001');
+insert into sys_role_menu values ('2', '1002');
+insert into sys_role_menu values ('2', '1003');
+insert into sys_role_menu values ('2', '1004');
+insert into sys_role_menu values ('2', '1005');
+insert into sys_role_menu values ('2', '1006');
+insert into sys_role_menu values ('2', '1007');
+insert into sys_role_menu values ('2', '1008');
+insert into sys_role_menu values ('2', '1009');
+insert into sys_role_menu values ('2', '1010');
+insert into sys_role_menu values ('2', '1011');
+insert into sys_role_menu values ('2', '1012');
+insert into sys_role_menu values ('2', '1013');
+insert into sys_role_menu values ('2', '1014');
+insert into sys_role_menu values ('2', '1015');
+insert into sys_role_menu values ('2', '1016');
+insert into sys_role_menu values ('2', '1017');
+insert into sys_role_menu values ('2', '1018');
+insert into sys_role_menu values ('2', '1019');
+insert into sys_role_menu values ('2', '1020');
+insert into sys_role_menu values ('2', '1021');
+insert into sys_role_menu values ('2', '1022');
+insert into sys_role_menu values ('2', '1023');
+insert into sys_role_menu values ('2', '1024');
+insert into sys_role_menu values ('2', '1025');
+insert into sys_role_menu values ('2', '1026');
+insert into sys_role_menu values ('2', '1027');
+insert into sys_role_menu values ('2', '1028');
+insert into sys_role_menu values ('2', '1029');
+insert into sys_role_menu values ('2', '1030');
+insert into sys_role_menu values ('2', '1031');
+insert into sys_role_menu values ('2', '1032');
+insert into sys_role_menu values ('2', '1033');
+insert into sys_role_menu values ('2', '1034');
+insert into sys_role_menu values ('2', '1035');
+insert into sys_role_menu values ('2', '1036');
+insert into sys_role_menu values ('2', '1037');
+insert into sys_role_menu values ('2', '1038');
+insert into sys_role_menu values ('2', '1039');
+insert into sys_role_menu values ('2', '1040');
+insert into sys_role_menu values ('2', '1041');
+insert into sys_role_menu values ('2', '1042');
+insert into sys_role_menu values ('2', '1043');
+insert into sys_role_menu values ('2', '1044');
+insert into sys_role_menu values ('2', '1045');
+insert into sys_role_menu values ('2', '1046');
+insert into sys_role_menu values ('2', '1047');
+insert into sys_role_menu values ('2', '1048');
+insert into sys_role_menu values ('2', '1049');
+insert into sys_role_menu values ('2', '1050');
+insert into sys_role_menu values ('2', '1051');
+insert into sys_role_menu values ('2', '1052');
+insert into sys_role_menu values ('2', '1053');
+insert into sys_role_menu values ('2', '1054');
+insert into sys_role_menu values ('2', '1055');
+insert into sys_role_menu values ('2', '1056');
+insert into sys_role_menu values ('2', '1057');
+insert into sys_role_menu values ('2', '1058');
+insert into sys_role_menu values ('2', '1059');
+insert into sys_role_menu values ('2', '1060');
+
+-- ----------------------------
+-- 8、角色和部门关联表 角色1-N部门
+-- ----------------------------
+drop table if exists sys_role_dept;
+create table sys_role_dept (
+ role_id bigint(20) not null comment '角色ID',
+ dept_id bigint(20) not null comment '部门ID',
+ primary key(role_id, dept_id)
+) engine=innodb comment = '角色和部门关联表';
+
+-- ----------------------------
+-- 初始化-角色和部门关联表数据
+-- ----------------------------
+insert into sys_role_dept values ('2', '100');
+insert into sys_role_dept values ('2', '101');
+insert into sys_role_dept values ('2', '105');
+
+
+-- ----------------------------
+-- 9、用户与岗位关联表 用户1-N岗位
+-- ----------------------------
+drop table if exists sys_user_post;
+create table sys_user_post
+(
+ user_id bigint(20) not null comment '用户ID',
+ post_id bigint(20) not null comment '岗位ID',
+ primary key (user_id, post_id)
+) engine=innodb comment = '用户与岗位关联表';
+
+-- ----------------------------
+-- 初始化-用户与岗位关联表数据
+-- ----------------------------
+insert into sys_user_post values ('1', '1');
+insert into sys_user_post values ('2', '2');
+
+
+-- ----------------------------
+-- 10、操作日志记录
+-- ----------------------------
+drop table if exists sys_oper_log;
+create table sys_oper_log (
+ oper_id bigint(20) not null auto_increment comment '日志主键',
+ title varchar(50) default '' comment '模块标题',
+ business_type int(2) default 0 comment '业务类型(0其它 1新增 2修改 3删除)',
+ method varchar(200) default '' comment '方法名称',
+ request_method varchar(10) default '' comment '请求方式',
+ operator_type int(1) default 0 comment '操作类别(0其它 1后台用户 2手机端用户)',
+ oper_name varchar(50) default '' comment '操作人员',
+ dept_name varchar(50) default '' comment '部门名称',
+ oper_url varchar(255) default '' comment '请求URL',
+ oper_ip varchar(128) default '' comment '主机地址',
+ oper_location varchar(255) default '' comment '操作地点',
+ oper_param varchar(2000) default '' comment '请求参数',
+ json_result varchar(2000) default '' comment '返回参数',
+ status int(1) default 0 comment '操作状态(0正常 1异常)',
+ error_msg varchar(2000) default '' comment '错误消息',
+ oper_time datetime comment '操作时间',
+ cost_time bigint(20) default 0 comment '消耗时间',
+ primary key (oper_id),
+ key idx_sys_oper_log_bt (business_type),
+ key idx_sys_oper_log_s (status),
+ key idx_sys_oper_log_ot (oper_time)
+) engine=innodb auto_increment=100 comment = '操作日志记录';
+
+
+-- ----------------------------
+-- 11、字典类型表
+-- ----------------------------
+drop table if exists sys_dict_type;
+create table sys_dict_type
+(
+ dict_id bigint(20) not null auto_increment comment '字典主键',
+ dict_name varchar(100) default '' comment '字典名称',
+ dict_type varchar(100) default '' comment '字典类型',
+ status char(1) default '0' comment '状态(0正常 1停用)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (dict_id),
+ unique (dict_type)
+) engine=innodb auto_increment=100 comment = '字典类型表';
+
+insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表');
+insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表');
+insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表');
+insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表');
+insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表');
+insert into sys_dict_type values(6, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表');
+insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表');
+insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表');
+insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表');
+insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表');
+
+
+-- ----------------------------
+-- 12、字典数据表
+-- ----------------------------
+drop table if exists sys_dict_data;
+create table sys_dict_data
+(
+ dict_code bigint(20) not null auto_increment comment '字典编码',
+ dict_sort int(4) default 0 comment '字典排序',
+ dict_label varchar(100) default '' comment '字典标签',
+ dict_value varchar(100) default '' comment '字典键值',
+ dict_type varchar(100) default '' comment '字典类型',
+ css_class varchar(100) default null comment '样式属性(其他样式扩展)',
+ list_class varchar(100) default null comment '表格回显样式',
+ is_default char(1) default 'N' comment '是否默认(Y是 N否)',
+ status char(1) default '0' comment '状态(0正常 1停用)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (dict_code)
+) engine=innodb auto_increment=100 comment = '字典数据表';
+
+insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男');
+insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女');
+insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知');
+insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单');
+insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单');
+insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
+insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
+insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
+insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
+insert into sys_dict_data values(10, 1, '默认', 'DEFAULT', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组');
+insert into sys_dict_data values(11, 2, '系统', 'SYSTEM', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '系统分组');
+insert into sys_dict_data values(12, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是');
+insert into sys_dict_data values(13, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否');
+insert into sys_dict_data values(14, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知');
+insert into sys_dict_data values(15, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告');
+insert into sys_dict_data values(16, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
+insert into sys_dict_data values(17, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态');
+insert into sys_dict_data values(18, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他操作');
+insert into sys_dict_data values(19, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作');
+insert into sys_dict_data values(20, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作');
+insert into sys_dict_data values(21, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作');
+insert into sys_dict_data values(22, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作');
+insert into sys_dict_data values(23, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作');
+insert into sys_dict_data values(24, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作');
+insert into sys_dict_data values(25, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作');
+insert into sys_dict_data values(26, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作');
+insert into sys_dict_data values(27, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作');
+insert into sys_dict_data values(28, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态');
+insert into sys_dict_data values(29, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
+
+
+-- ----------------------------
+-- 13、参数配置表
+-- ----------------------------
+drop table if exists sys_config;
+create table sys_config (
+ config_id int(5) not null auto_increment comment '参数主键',
+ config_name varchar(100) default '' comment '参数名称',
+ config_key varchar(100) default '' comment '参数键名',
+ config_value varchar(500) default '' comment '参数键值',
+ config_type char(1) default 'N' comment '系统内置(Y是 N否)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (config_id)
+) engine=innodb auto_increment=100 comment = '参数配置表';
+
+insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' );
+insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' );
+insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' );
+insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)');
+insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)');
+insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
+
+
+-- ----------------------------
+-- 14、系统访问记录
+-- ----------------------------
+drop table if exists sys_logininfor;
+create table sys_logininfor (
+ info_id bigint(20) not null auto_increment comment '访问ID',
+ user_name varchar(50) default '' comment '用户账号',
+ ipaddr varchar(128) default '' comment '登录IP地址',
+ login_location varchar(255) default '' comment '登录地点',
+ browser varchar(50) default '' comment '浏览器类型',
+ os varchar(50) default '' comment '操作系统',
+ status char(1) default '0' comment '登录状态(0成功 1失败)',
+ msg varchar(255) default '' comment '提示消息',
+ login_time datetime comment '访问时间',
+ primary key (info_id),
+ key idx_sys_logininfor_s (status),
+ key idx_sys_logininfor_lt (login_time)
+) engine=innodb auto_increment=100 comment = '系统访问记录';
+
+
+-- ----------------------------
+-- 15、定时任务调度表
+-- ----------------------------
+drop table if exists sys_job;
+create table sys_job (
+ job_id bigint(20) not null auto_increment comment '任务ID',
+ job_name varchar(64) default '' comment '任务名称',
+ job_group varchar(64) default 'DEFAULT' comment '任务组名',
+ invoke_target varchar(500) not null comment '调用目标字符串',
+ cron_expression varchar(255) default '' comment 'cron执行表达式',
+ misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
+ concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)',
+ status char(1) default '0' comment '状态(0正常 1暂停)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default '' comment '备注信息',
+ primary key (job_id, job_name, job_group)
+) engine=innodb auto_increment=100 comment = '定时任务调度表';
+
+insert into sys_job values(1, '系统默认(无参)', 'DEFAULT', 'ryTask.ryNoParams', '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
+insert into sys_job values(2, '系统默认(有参)', 'DEFAULT', 'ryTask.ryParams(\'ry\')', '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
+insert into sys_job values(3, '系统默认(多参)', 'DEFAULT', 'ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
+
+
+-- ----------------------------
+-- 16、定时任务调度日志表
+-- ----------------------------
+drop table if exists sys_job_log;
+create table sys_job_log (
+ job_log_id bigint(20) not null auto_increment comment '任务日志ID',
+ job_name varchar(64) not null comment '任务名称',
+ job_group varchar(64) not null comment '任务组名',
+ invoke_target varchar(500) not null comment '调用目标字符串',
+ job_message varchar(500) comment '日志信息',
+ status char(1) default '0' comment '执行状态(0正常 1失败)',
+ exception_info varchar(2000) default '' comment '异常信息',
+ create_time datetime comment '创建时间',
+ primary key (job_log_id)
+) engine=innodb comment = '定时任务调度日志表';
+
+
+-- ----------------------------
+-- 17、通知公告表
+-- ----------------------------
+drop table if exists sys_notice;
+create table sys_notice (
+ notice_id int(4) not null auto_increment comment '公告ID',
+ notice_title varchar(50) not null comment '公告标题',
+ notice_type char(1) not null comment '公告类型(1通知 2公告)',
+ notice_content longblob default null comment '公告内容',
+ status char(1) default '0' comment '公告状态(0正常 1关闭)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(255) default null comment '备注',
+ primary key (notice_id)
+) engine=innodb auto_increment=10 comment = '通知公告表';
+
+-- ----------------------------
+-- 初始化-公告信息表数据
+-- ----------------------------
+insert into sys_notice values('1', '温馨提醒:2018-07-01 若依新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员');
+insert into sys_notice values('2', '维护通知:2018-07-01 若依系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员');
+
+
+-- ----------------------------
+-- 18、代码生成业务表
+-- ----------------------------
+drop table if exists gen_table;
+create table gen_table (
+ table_id bigint(20) not null auto_increment comment '编号',
+ table_name varchar(200) default '' comment '表名称',
+ table_comment varchar(500) default '' comment '表描述',
+ sub_table_name varchar(64) default null comment '关联子表的表名',
+ sub_table_fk_name varchar(64) default null comment '子表关联的外键名',
+ class_name varchar(100) default '' comment '实体类名称',
+ tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)',
+ tpl_web_type varchar(30) default '' comment '前端模板类型(element-ui模版 element-plus模版)',
+ package_name varchar(100) comment '生成包路径',
+ module_name varchar(30) comment '生成模块名',
+ business_name varchar(30) comment '生成业务名',
+ function_name varchar(50) comment '生成功能名',
+ function_author varchar(50) comment '生成功能作者',
+ gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)',
+ gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)',
+ options varchar(1000) comment '其它生成选项',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default null comment '备注',
+ primary key (table_id)
+) engine=innodb auto_increment=1 comment = '代码生成业务表';
+
+
+-- ----------------------------
+-- 19、代码生成业务表字段
+-- ----------------------------
+drop table if exists gen_table_column;
+create table gen_table_column (
+ column_id bigint(20) not null auto_increment comment '编号',
+ table_id bigint(20) comment '归属表编号',
+ column_name varchar(200) comment '列名称',
+ column_comment varchar(500) comment '列描述',
+ column_type varchar(100) comment '列类型',
+ java_type varchar(500) comment 'JAVA类型',
+ java_field varchar(200) comment 'JAVA字段名',
+ is_pk char(1) comment '是否主键(1是)',
+ is_increment char(1) comment '是否自增(1是)',
+ is_required char(1) comment '是否必填(1是)',
+ is_insert char(1) comment '是否为插入字段(1是)',
+ is_edit char(1) comment '是否编辑字段(1是)',
+ is_list char(1) comment '是否列表字段(1是)',
+ is_query char(1) comment '是否查询字段(1是)',
+ query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)',
+ html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)',
+ dict_type varchar(200) default '' comment '字典类型',
+ sort int comment '排序',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ primary key (column_id)
+) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
\ No newline at end of file