diff --git a/.gitignore b/.gitignore
index 549e00a..d65ea6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,7 @@ build/
### VS Code ###
.vscode/
+
+### Custom ###
+/application-config.yml
+data
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..c21afa2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+FROM eclipse-temurin:17-jdk-alpine
+LABEL authors="FatttSnake"
+
+VOLUME /data
+
+ARG EXTRACTED=target/extracted
+COPY ${EXTRACTED}/dependencies/ /
+COPY ${EXTRACTED}/spring-boot-loader/ /
+COPY ${EXTRACTED}/snapshot-dependencies/ /
+RUN true
+COPY ${EXTRACTED}/application/ /
+
+ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher", "--spring.config.additional-location=file:data/"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f634ba
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+
+
+
+
+ API of Oxygen Toolbox
+
+
+
+
+# Overview ([ZH](README_zh.md), EN)
+
+This project is the backend API of Oxygen Toolbox. Provides tool store, tool management, authentication, user management and other functions.
+
+# Requires
+
+- Java 17+
+- MySQL
+- Redis
+
+# Related projects
+
+[Web UI of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-ui)
+
+[Desktop Client of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-desktop)
+
+[Android Client of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-android)
+
+# Quick Start
+
+1. First run, generate configuration file template
+
+```shell
+java -jar oxygen-api.jar
+```
+
+2. Copy the `application-config.example.yml` file in the `data` directory to the running directory and rename it to `application-config.yml`
+
+```shell
+cp ./data/application-config.example.yml application-config.yml
+```
+
+3. Edit the content of the configuration file `application-config.yml`
+
+
+4. Run again
+
+```shell
+java -jar oxygen-api.jar
+```
+
+# Security
+
+Integration with Spring Security and add other filter for jwt token process. The secret key is stored in `application-config.yml`.
+
+# Database
+
+Two databases, MySQL + SQLite, are used. MySQL is used to store key data, and SQLite is used to store logs and other data that require a large amount of reading and writing.
+
+# Q&A
+
+> **Q: What is the default administrator account and password?**
+>
+> A: If configured in `application-config.yml` before initializing the database, use the specified account and password. If not configured, a random password will be generated by default. See the console output for details.
+
+> **Q: Do I need to initialize the database?**
+>
+> A: This project uses `Flyway` to automatically initialize the database without manually defining the data table structure. To ensure data security, please back up the database before upgrading.
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..c6b2738
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,77 @@
+
+
+
+
+ API of Oxygen Toolbox
+
+
+
+
+# 概述 (ZH, [EN](README.md))
+
+本项目为 Oxygen Toolbox 的后端 API。提供工具商店、工具管理、认证鉴权、用户管理等功能。
+
+# 环境要求
+
+- Java 17+
+- MySQL
+- Redis
+
+# 关联项目
+
+[Web UI of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-ui)
+
+[Desktop Client of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-desktop)
+
+[Android Client of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-android)
+
+# 快速开始
+
+1. 初次运行,生成配置文件模板
+
+```shell
+java -jar oxygen-api.jar
+```
+
+2. 将 `data` 目录下的 `application-config.example.yml` 文件复制到运行目录下,并重命名为 `application-config.yml`
+
+```shell
+cp ./data/application-config.example.yml application-config.yml
+```
+
+3. 编辑配置文件 `application-config.yml` 内容
+
+
+4. 再次运行
+
+```shell
+java -jar oxygen-api.jar
+```
+
+# 安全
+
+集成 Spring Security 并采用 jwt 令牌, 密钥存储在 `application-config.yml` 中。
+
+# 数据库
+
+采用 MySQL + SQLite 双数据库,MySQL 用于存放关键数据,SQLite 用于存放日志等需要大量读写的数据。
+
+# Q&A
+
+> **Q: 默认管理员账号和密码是什么?**
+>
+> A: 初始化数据库前配置在 `application-config.yml` 中,则使用所指定账号密码。未配置则默认生成随机密码,详见控制台输出。
+
+> **Q: 是否需要初始化数据库?**
+>
+> A: 本项目采用 `Flyway` 自动初始化数据库,无需手动定义数据表结构。为保证数据安全,升级时请先备份数据库。
diff --git a/build-docker.sh b/build-docker.sh
new file mode 100644
index 0000000..38a0c47
--- /dev/null
+++ b/build-docker.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+JAR_NAME=`ls target | grep oxygen-api- | grep -vE "original|asc|pom|javadoc"`
+JAR_VERSION=${JAR_NAME%.*}
+JAR_VERSION=${JAR_VERSION#*-}
+JAR_VERSION=${JAR_VERSION#*-}
+BUILD_TIME=$(date "+%Y%m%d%H%M%S")
+
+echo ${BUILD_TIME} > .build_time
+
+mkdir target/extracted
+java -Djarmode=layertools -jar target/${JAR_NAME} extract --destination target/extracted
+
+if [[ "${JAR_VERSION}" =~ ^.*SNAPSHOT$ ]]
+then
+ docker build -t ${DOCKER_HUB_URL}/oxygen-api:snapshot-latest -t ${DOCKER_HUB_URL}/oxygen-api:${JAR_VERSION} -t ${DOCKER_HUB_URL}/oxygen-api:${JAR_VERSION}-${BUILD_TIME} .
+else
+ docker build -t ${DOCKER_HUB_URL}/oxygen-api:latest -t ${DOCKER_HUB_URL}/oxygen-api:${JAR_VERSION} -t ${DOCKER_HUB_URL}/oxygen-api:${JAR_VERSION}-${BUILD_TIME} .
+fi
\ No newline at end of file
diff --git a/doc/database.drawio b/doc/database.drawio
new file mode 100644
index 0000000..1106267
--- /dev/null
+++ b/doc/database.drawio
@@ -0,0 +1,2073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/logo.svg b/doc/logo.svg
new file mode 100644
index 0000000..6b67342
--- /dev/null
+++ b/doc/logo.svg
@@ -0,0 +1 @@
+
diff --git a/doc/permission.drawio b/doc/permission.drawio
new file mode 100644
index 0000000..2ca4a6e
--- /dev/null
+++ b/doc/permission.drawio
@@ -0,0 +1,520 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index ee5348b..f95846e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,86 +5,96 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.4
+ 3.2.1
- top.fatweb
+
+ top.fatweb.oxygen
api
- 0.0.1-SNAPSHOT
- fatweb-api
- fatweb-api
+ 1.0.0
+
+ oxygen-api
+ API of Oxygen Toolbox
+ https://github.com/FatttSnake/oxygen-api
+
17
- 1.8.22
+ 1.9.21
+ ${maven.build.timestamp}
+ yyyy-MM-dd'T'HH:mm:ss
+ 3.0.3
+ 2.3
+ 3.5.7
+ 4.3.1
+ 9.22.3
+ 4.4.0
+ 1.1.0
+ 6.4.9
+ 3.5.3
+ 4.4.0
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- com.fasterxml.jackson.module
- jackson-module-kotlin
-
-
- org.jetbrains.kotlin
- kotlin-reflect
-
-
- org.jetbrains.kotlin
- kotlin-stdlib
-
-
- org.springframework.boot
- spring-boot-devtools
- runtime
- true
-
-
- org.projectlombok
- lombok
- true
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
- org.springframework.security
- spring-security-test
- test
-
-
+
+ FatWeb
+ https://fatweb.top
+
+
+
+
+ GNU General Public License v3.0 or later
+ https://www.gnu.org/licenses/gpl-3.0-standalone.html
+ repo
+
+
+
+
+
+ fatttsnake
+ FatttSnake
+ fatttsnake@gmail.com
+
+
+ ${project.name}-${project.version}
${project.basedir}/src/main/kotlin
${project.basedir}/src/test/kotlin
-
- org.graalvm.buildtools
- native-maven-plugin
-
org.springframework.boot
spring-boot-maven-plugin
-
-
-
- org.projectlombok
- lombok
-
-
-
org.jetbrains.kotlin
kotlin-maven-plugin
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ src/main/kotlin
+ target/generated-sources/annotations
+
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+ src/main/kotlin
+ target/generated-sources/annotations
+
+
+
+
-Xjsr305=strict
@@ -101,7 +111,229 @@
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 3.1.2
+
+
+ org.jetbrains.dokka
+ dokka-maven-plugin
+ 1.9.10
+
+
+ pre-site
+
+ dokka
+
+
+
+ attach-javadocJar
+ package
+
+ javadocJar
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ com.github.lianjiatech
+ retrofit-spring-boot-starter
+ ${retrofit.version}
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+ org.apache.velocity
+ velocity-engine-core
+ ${velocity.version}
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+ org.xerial
+ sqlite-jdbc
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot3-starter
+ ${dynamic-datasource.version}
+
+
+ org.flywaydb
+ flyway-mysql
+ ${flyway.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ com.auth0
+ java-jwt
+ ${jwt.version}
+
+
+ top.fatweb
+ avatar-generator
+ ${avatar-generator.version}
+
+
+ com.github.oshi
+ oshi-core
+ ${oshi.version}
+
+
+ commons-codec
+ commons-codec
+
+
+ com.google.zxing
+ core
+ ${zxing.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter-test
+ ${mybatis-plus.version}
+ test
+
+
+
+
+
+ dev
+
+ true
+
+ env
+ dev
+
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-jakarta-spring-boot-starter
+ ${knife4j.version}
+
+
+
+
+ release
+
+
+ env
+ release
+
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-jakarta-spring-boot-starter
+ ${knife4j.version}
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-ui
+
+
+ org.webjars
+ swagger-ui
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifact
+ verify
+
+ sign
+
+
+
+
+
+
+
+
diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt
deleted file mode 100644
index f77dbd7..0000000
--- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package top.fatweb.api
-
-import org.springframework.boot.autoconfigure.SpringBootApplication
-import org.springframework.boot.runApplication
-
-@SpringBootApplication
-class FatWebApiApplication
-
-fun main(args: Array) {
- runApplication(*args)
-}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/OxygenApiApplication.kt b/src/main/kotlin/top/fatweb/oxygen/api/OxygenApiApplication.kt
new file mode 100644
index 0000000..6bfaadb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/OxygenApiApplication.kt
@@ -0,0 +1,67 @@
+package top.fatweb.oxygen.api
+
+import org.slf4j.LoggerFactory
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.scheduling.annotation.EnableScheduling
+import org.springframework.transaction.annotation.EnableTransactionManagement
+import java.io.File
+import java.util.*
+
+/**
+ * Application main class
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@SpringBootApplication
+@EnableTransactionManagement
+@EnableScheduling
+class OxygenApiApplication
+
+/**
+ * Main function
+ *
+ * @param args
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+fun main(args: Array) {
+ val logger = LoggerFactory.getLogger("main")
+
+ if (!File("data").isDirectory) {
+ if (!File("data").mkdir()) {
+ logger.error("Can not create directory 'data', please try again later.")
+ return
+ }
+ }
+
+ if (!File("data/db").isDirectory) {
+ if (!File("data/db").mkdir()) {
+ logger.error("Can not create directory 'data/db', please try again later.")
+ return
+ }
+ }
+
+ if (!File("data/db/sqlite.db").isFile || File("data/db/sqlite.db").inputStream()
+ .use { it.readNBytes(15).toString(Charsets.UTF_8) != "SQLite format 3" }
+ ) {
+ logger.warn("The 'data/db/sqlite.db' database is lost or damaged, recreating...")
+ if (File("data/db/sqlite.db").exists() && !File("data/db/sqlite.db").delete()) {
+ logger.error("Can not recreate database 'data/db/sqlite.db', please try again later.")
+ }
+ }
+
+ if (File("application-config.yml").exists() || File("data/application-config.yml").exists()) {
+ runApplication(*args)
+ } else {
+ logger.warn("File 'application-config.yml' cannot be found in the running path or the data path. The configuration file template 'application-config.example.yml' has been created in directory 'data'. Please change the configuration file content, move it to the running path, rename it to 'application-config.yml', and then restart the server.")
+ OxygenApiApplication::class.java.getResource("/application-config-template.yml")?.readText()?.let {
+ File("data/application-config.example.yml").writeText(
+ it.replace(
+ "\$uuid\$", UUID.randomUUID().toString()
+ )
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/annotation/ApiController.kt b/src/main/kotlin/top/fatweb/oxygen/api/annotation/ApiController.kt
new file mode 100644
index 0000000..5805c4e
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/annotation/ApiController.kt
@@ -0,0 +1,32 @@
+package top.fatweb.oxygen.api.annotation
+
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.core.annotation.AliasFor
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * API controller annotation
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tag
+ * @see RequestMapping
+ * @see RestController
+ */
+@Tag(name = "")
+@RequestMapping
+@RestController
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApiController(
+ val version: Int = 1,
+
+ @get:AliasFor(annotation = RestController::class, attribute = "value") val value: String = "",
+
+ @get:AliasFor(annotation = RequestMapping::class, attribute = "path") val path: Array = [""],
+
+ @get:AliasFor(annotation = Tag::class, attribute = "name") val name: String,
+
+ @get:AliasFor(annotation = Tag::class, attribute = "description") val description: String
+)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/annotation/BaseController.kt b/src/main/kotlin/top/fatweb/oxygen/api/annotation/BaseController.kt
new file mode 100644
index 0000000..598555d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/annotation/BaseController.kt
@@ -0,0 +1,27 @@
+package top.fatweb.oxygen.api.annotation
+
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.core.annotation.AliasFor
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Base controller annotation
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RequestMapping
+ * @see RestController
+ */
+@Tag(name = "")
+@RequestMapping
+@RestController
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class BaseController(
+ @get:AliasFor(annotation = RequestMapping::class, attribute = "path") val path: Array = [""],
+
+ @get:AliasFor(annotation = Tag::class, attribute = "name") val name: String,
+
+ @get:AliasFor(annotation = Tag::class, attribute = "description") val description: String
+)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/annotation/EventLogRecord.kt b/src/main/kotlin/top/fatweb/oxygen/api/annotation/EventLogRecord.kt
new file mode 100644
index 0000000..f0e023b
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/annotation/EventLogRecord.kt
@@ -0,0 +1,15 @@
+package top.fatweb.oxygen.api.annotation
+
+import top.fatweb.oxygen.api.entity.system.EventLog
+
+/**
+ * Event log record annotation
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class EventLogRecord(
+ val event: EventLog.Event
+)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/annotation/HiddenController.kt b/src/main/kotlin/top/fatweb/oxygen/api/annotation/HiddenController.kt
new file mode 100644
index 0000000..90287fb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/annotation/HiddenController.kt
@@ -0,0 +1,24 @@
+package top.fatweb.oxygen.api.annotation
+
+import io.swagger.v3.oas.annotations.Hidden
+import org.springframework.core.annotation.AliasFor
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Hidden controller annotation
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Hidden
+ * @see RequestMapping
+ * @see RestController
+ */
+@Hidden
+@RequestMapping
+@RestController
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class HiddenController(
+ @get:AliasFor(annotation = RequestMapping::class, attribute = "path") val path: Array = [""]
+)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/annotation/Trim.kt b/src/main/kotlin/top/fatweb/oxygen/api/annotation/Trim.kt
new file mode 100644
index 0000000..f3bdc81
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/annotation/Trim.kt
@@ -0,0 +1,15 @@
+package top.fatweb.oxygen.api.annotation
+
+import java.lang.annotation.Inherited
+
+
+/**
+ * Trim string annotation
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+@Inherited
+annotation class Trim
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/aop/EventLogInterceptor.kt b/src/main/kotlin/top/fatweb/oxygen/api/aop/EventLogInterceptor.kt
new file mode 100644
index 0000000..5a61beb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/aop/EventLogInterceptor.kt
@@ -0,0 +1,58 @@
+package top.fatweb.oxygen.api.aop
+
+import org.aspectj.lang.JoinPoint
+import org.aspectj.lang.annotation.AfterReturning
+import org.aspectj.lang.annotation.Aspect
+import org.aspectj.lang.annotation.Pointcut
+import org.aspectj.lang.reflect.MethodSignature
+import org.springframework.stereotype.Component
+import top.fatweb.oxygen.api.annotation.EventLogRecord
+import top.fatweb.oxygen.api.service.system.IEventLogService
+import top.fatweb.oxygen.api.util.WebUtil
+import top.fatweb.oxygen.api.vo.permission.LoginVo
+import top.fatweb.oxygen.api.vo.permission.RegisterVo
+
+/**
+ * Event log record interceptor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IEventLogService
+ */
+@Aspect
+@Component
+class EventLogInterceptor(
+ private val eventLogService: IEventLogService
+) {
+ /**
+ * Event log record pointcut
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Pointcut("@annotation(top.fatweb.oxygen.api.annotation.EventLogRecord)")
+ fun eventLogPointcut() {
+ }
+
+ /**
+ * Do after event log record pointcut
+ *
+ * @param joinPoint Join point
+ * @param retValue Return value
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see JoinPoint
+ */
+ @AfterReturning(value = "eventLogPointcut()", returning = "retValue")
+ fun doAfter(joinPoint: JoinPoint, retValue: Any?) {
+ val annotation = (joinPoint.signature as MethodSignature).method.getAnnotation(EventLogRecord::class.java)
+
+ val userId = WebUtil.getLoginUserId() ?: when (retValue) {
+ is LoginVo -> retValue.userId!!
+ is RegisterVo -> retValue.userId!!
+ else -> -1
+ }
+
+ eventLogService.saveEvent(annotation, userId)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/aop/SysLogInterceptor.kt b/src/main/kotlin/top/fatweb/oxygen/api/aop/SysLogInterceptor.kt
new file mode 100644
index 0000000..5c8a27c
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/aop/SysLogInterceptor.kt
@@ -0,0 +1,136 @@
+package top.fatweb.oxygen.api.aop
+
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.core.MethodParameter
+import org.springframework.http.MediaType
+import org.springframework.http.converter.HttpMessageConverter
+import org.springframework.http.server.ServerHttpRequest
+import org.springframework.http.server.ServerHttpResponse
+import org.springframework.web.bind.annotation.ControllerAdvice
+import org.springframework.web.servlet.HandlerInterceptor
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.entity.system.SysLog
+import top.fatweb.oxygen.api.service.system.ISysLogService
+import top.fatweb.oxygen.api.util.WebUtil
+import top.fatweb.oxygen.api.vo.permission.LoginVo
+import java.net.URI
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.time.temporal.ChronoUnit
+import java.util.*
+import java.util.concurrent.Executor
+
+/**
+ * System log interceptor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Executor
+ * @see ISysLogService
+ * @see HandlerInterceptor
+ * @see ResponseBodyAdvice
+ */
+@ControllerAdvice
+class SysLogInterceptor(
+ @Qualifier("applicationTaskExecutor") private val customThreadPoolTaskExecutor: Executor,
+ private val sysLogService: ISysLogService
+) : HandlerInterceptor, ResponseBodyAdvice {
+ private val sysLogThreadLocal = ThreadLocal()
+ private val resultThreadLocal = ThreadLocal()
+
+ override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
+ val sysLog = SysLog().apply {
+ operateUserId = WebUtil.getLoginUserId() ?: -1
+ startTime = LocalDateTime.now(ZoneOffset.UTC)
+ requestUri = URI(request.requestURI).path
+ requestParams = formatParams(request.parameterMap)
+ requestMethod = request.method
+ requestIp = WebUtil.getRequestIp(request)
+ requestServerAddress = "${request.scheme}://${request.serverName}:${request.serverPort}"
+ userAgent = request.getHeader("User-Agent")
+ }
+
+ sysLogThreadLocal.set(sysLog)
+
+ return true
+ }
+
+ override fun afterCompletion(
+ request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?
+ ) {
+ val sysLog = sysLogThreadLocal.get()
+ val result = resultThreadLocal.get()
+ sysLog.endTime = LocalDateTime.now(ZoneOffset.UTC)
+ sysLog.executeTime = ChronoUnit.MILLIS.between(sysLog.startTime, sysLog.endTime)
+ if (result is ResponseResult<*>) {
+ if (result.success) {
+ sysLog.apply {
+ logType = requestUri?.let {
+ when {
+ it.startsWith("/login") -> SysLog.LogType.LOGIN
+ it.startsWith("/logout") -> SysLog.LogType.LOGOUT
+ it.startsWith("/register") -> SysLog.LogType.REGISTER
+ it.startsWith("/system/statistics/") -> SysLog.LogType.STATISTICS
+ it.startsWith("/api/") -> SysLog.LogType.API
+ else -> SysLog.LogType.INFO
+ }
+ } ?: SysLog.LogType.INFO
+ exception = 0
+ }
+ if (result.data is LoginVo) {
+ sysLog.operateUserId = result.data.userId ?: -1
+ }
+ } else {
+ sysLog.apply {
+ logType = SysLog.LogType.ERROR
+ exception = 1
+ exceptionInfo = result.msg
+ }
+ }
+
+ customThreadPoolTaskExecutor.execute(SaveLogThread(sysLog, sysLogService))
+ }
+ sysLogThreadLocal.remove()
+ }
+
+ private fun formatParams(parameterMap: Map>): String {
+ val params = StringJoiner("&")
+
+ parameterMap.forEach {
+ params.add("${it.key}=${if (it.key.endsWith("password", true)) "*" else it.value.joinToString(",")}")
+ }
+
+ return params.toString()
+ }
+
+ private class SaveLogThread(val sysLog: SysLog, val sysLogService: ISysLogService) : Thread() {
+ override fun run() {
+ sysLog.operateTime = LocalDateTime.now(ZoneOffset.UTC)
+ sysLogService.save(sysLog)
+ }
+ }
+
+ override fun supports(returnType: MethodParameter, converterType: Class>): Boolean =
+ true
+
+ override fun beforeBodyWrite(
+ body: Any?,
+ returnType: MethodParameter,
+ selectedContentType: MediaType,
+ selectedConverterType: Class>,
+ request: ServerHttpRequest,
+ response: ServerHttpResponse
+ ): Any? {
+ resultThreadLocal.set(body)
+
+ if (body is ResponseResult<*> && body.code == ResponseCode.SYSTEM_ERROR.code) {
+ return ResponseResult.build(body.code, body.success, "fail", body.data)
+ }
+
+ return body
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/aop/TrimInterceptor.kt b/src/main/kotlin/top/fatweb/oxygen/api/aop/TrimInterceptor.kt
new file mode 100644
index 0000000..fae937f
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/aop/TrimInterceptor.kt
@@ -0,0 +1,87 @@
+package top.fatweb.oxygen.api.aop
+
+import org.aspectj.lang.JoinPoint
+import org.aspectj.lang.annotation.Aspect
+import org.aspectj.lang.annotation.Before
+import org.aspectj.lang.annotation.Pointcut
+import org.springframework.stereotype.Component
+import org.springframework.web.servlet.HandlerInterceptor
+import top.fatweb.oxygen.api.annotation.Trim
+import kotlin.reflect.KMutableProperty
+import kotlin.reflect.full.*
+import kotlin.reflect.jvm.isAccessible
+
+/**
+ * Trim string interceptor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HandlerInterceptor
+ */
+@Component
+@Aspect
+class TrimInterceptor : HandlerInterceptor {
+ /**
+ * Trim pointcut
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Pointcut("@annotation(top.fatweb.oxygen.api.annotation.Trim)")
+ fun trimPointcut() {
+ }
+
+ /**
+ * Do before trim pointcut
+ *
+ * @param joinPoint Join point
+ * @return Arguments
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see JoinPoint
+ */
+ @Before("trimPointcut()")
+ fun doBefore(joinPoint: JoinPoint): Any {
+ val args = joinPoint.args
+
+ args?.forEachIndexed { index, any ->
+ if (args[index]::class.hasAnnotation()) args[index] = trim(any)
+ }
+
+ return args
+ }
+
+ private fun trim(any: Any?): Any? {
+ any ?: return null
+
+ when (any) {
+ is Boolean, Short, Int, Long, Float, Double -> {
+ return any
+ }
+
+ is String -> {
+ return any.trim()
+ }
+
+ else -> {
+ val members = any::class.declaredMemberProperties
+ if (members.isEmpty()) {
+ return any
+ }
+ members.forEach {
+ if (!it.returnType.isSupertypeOf(String::class.starProjectedType)
+ || it !is KMutableProperty<*>
+ || !it.hasAnnotation()
+ ) {
+ return@forEach
+ }
+ it.isAccessible = true
+ if (it.call(any) != null) {
+ it.setter.call(any, (it.call(any) as String).trim())
+ }
+ }
+ return any
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/DateFormatConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/DateFormatConfig.kt
new file mode 100644
index 0000000..a2ff193
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/DateFormatConfig.kt
@@ -0,0 +1,56 @@
+package top.fatweb.oxygen.api.config
+
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
+import org.springframework.boot.jackson.JsonComponent
+import org.springframework.context.annotation.Bean
+import java.text.SimpleDateFormat
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+/**
+ * Date format configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@JsonComponent
+class DateFormatConfig {
+ /**
+ * The format of the time in response when request APIs
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @set:Value("\${spring.jackson.date-format}")
+ lateinit var dateFormat: String
+
+ /**
+ * The timezone of the time in response when request APIs
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see TimeZone
+ */
+ @set:Value("\${spring.jackson.time-zone}}")
+ lateinit var timeZone: TimeZone
+
+ @Bean
+ fun jackson2ObjectMapperBuilder() = Jackson2ObjectMapperBuilderCustomizer {
+ val dateFormat = SimpleDateFormat(dateFormat)
+ dateFormat.timeZone = timeZone
+ it.failOnEmptyBeans(false).failOnUnknownProperties(false)
+ .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).dateFormat(dateFormat)
+ }
+
+ @Bean
+ fun jackson2ObjectMapperBuilderCustomizer() =
+ Jackson2ObjectMapperBuilderCustomizer {
+ it.serializerByType(
+ LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateFormat))
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/FilterConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/FilterConfig.kt
new file mode 100644
index 0000000..54d659e
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/FilterConfig.kt
@@ -0,0 +1,22 @@
+package top.fatweb.oxygen.api.config
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import top.fatweb.oxygen.api.filter.ExceptionFilter
+
+/**
+ * Filter configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class FilterConfig {
+ @Bean
+ fun exceptionFilterRegistrationBean(exceptionFilter: ExceptionFilter): FilterRegistrationBean =
+ FilterRegistrationBean(exceptionFilter).apply {
+ setBeanName("exceptionFilter")
+ order = -100
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/FlywayConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/FlywayConfig.kt
new file mode 100644
index 0000000..9e34385
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/FlywayConfig.kt
@@ -0,0 +1,43 @@
+package top.fatweb.oxygen.api.config
+
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource
+import jakarta.annotation.PostConstruct
+import org.flywaydb.core.Flyway
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.DependsOn
+import top.fatweb.oxygen.api.properties.FlywayProperties
+import javax.sql.DataSource
+
+/**
+ * Flyway configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@DependsOn("flywayProperties")
+@Configuration
+class FlywayConfig(
+ private val dataSource: DataSource
+) {
+ @PostConstruct
+ fun migrateOrder() {
+ val ds = dataSource as DynamicRoutingDataSource
+ ds.dataSources.forEach { (k: String, v: DataSource?) ->
+ val flyway = Flyway.configure()
+ .dataSource(v)
+ .locations(*FlywayProperties.locations.map { "$it/$k" }.toTypedArray())
+ .baselineOnMigrate(FlywayProperties.baselineOnMigrate)
+ .table(FlywayProperties.table)
+ .outOfOrder(FlywayProperties.outOfOrder)
+ .validateOnMigrate(FlywayProperties.validateOnMigrate)
+ .encoding(FlywayProperties.encoding)
+ .sqlMigrationPrefix(FlywayProperties.sqlMigrationPrefix)
+ .sqlMigrationSeparator(FlywayProperties.sqlMigrationSeparator)
+ .sqlMigrationSuffixes(*FlywayProperties.sqlMigrationSuffixes.toTypedArray())
+ .baselineVersion(FlywayProperties.baselineVersion)
+ .load()
+ flyway.migrate()
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/InitConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/InitConfig.kt
new file mode 100644
index 0000000..7e4d123
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/InitConfig.kt
@@ -0,0 +1,69 @@
+package top.fatweb.oxygen.api.config
+
+import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
+import jakarta.annotation.PostConstruct
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.context.annotation.DependsOn
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.stereotype.Component
+import top.fatweb.avatargenerator.GitHubAvatar
+import top.fatweb.oxygen.api.entity.permission.User
+import top.fatweb.oxygen.api.entity.permission.UserInfo
+import top.fatweb.oxygen.api.properties.AdminProperties
+import top.fatweb.oxygen.api.service.permission.IUserInfoService
+import top.fatweb.oxygen.api.service.permission.IUserService
+import top.fatweb.oxygen.api.util.StrUtil
+
+/**
+ * Application initialization configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IUserService
+ * @see IUserInfoService
+ * @see PasswordEncoder
+ */
+@DependsOn("adminProperties")
+@Component
+class InitConfig(
+ private val userService: IUserService,
+ private val userInfoService: IUserInfoService,
+ private val passwordEncoder: PasswordEncoder
+) {
+ private val logger: Logger = LoggerFactory.getLogger(this::class.java)
+
+ @PostConstruct
+ fun init() {
+ if (!userService.exists(KtQueryWrapper(User()).eq(User::id, 0))) {
+ userInfoService.remove(KtQueryWrapper(UserInfo()).eq(UserInfo::userId, 0))
+
+ val rawPassword = AdminProperties.password ?: let {
+ logger.warn("No default administrator password is set, a randomly generated password will be used")
+ StrUtil.getRandomPassword(10)
+ }
+ val encodedPassword = passwordEncoder.encode(rawPassword)
+
+ val user = User().apply {
+ id = 0
+ username = AdminProperties.username
+ password = encodedPassword
+ locking = 0
+ enable = 1
+ }
+ val userInfo = UserInfo().apply {
+ userId = 0
+ nickname = AdminProperties.nickname
+ avatar =
+ GitHubAvatar.newAvatarBuilder().build().createAsBase64((Long.MIN_VALUE..Long.MAX_VALUE).random())
+ email = AdminProperties.email
+ }
+
+ if (userService.save(user) && userInfoService.save(userInfo)) {
+ logger.warn("First startup, create administrator - username: admin, password: $rawPassword")
+ logger.warn("This information will only be shown once. Please change your password promptly after logging in.")
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt
new file mode 100644
index 0000000..795435d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt
@@ -0,0 +1,27 @@
+package top.fatweb.oxygen.api.config
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.json.JsonMapper
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import retrofit2.converter.jackson.JacksonConverterFactory
+
+/**
+ * Jackson configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class JacksonConfig {
+ @Bean
+ fun jacksonConverterFactory(): JacksonConverterFactory =
+ JacksonConverterFactory.create(
+ JsonMapper.builder()
+ .findAndAddModules()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .serializationInclusion(JsonInclude.Include.NON_NULL)
+ .build()
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/MybatisPlusConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/MybatisPlusConfig.kt
new file mode 100644
index 0000000..0d163bd
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/MybatisPlusConfig.kt
@@ -0,0 +1,23 @@
+package top.fatweb.oxygen.api.config
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+/**
+ * Mybatis-plus configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class MybatisPlusConfig {
+ @Bean
+ fun mybatisPlusInterceptor(): MybatisPlusInterceptor =
+ MybatisPlusInterceptor().apply {
+ addInnerInterceptor(OptimisticLockerInnerInterceptor())
+ addInnerInterceptor(PaginationInnerInterceptor())
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/RedisConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/RedisConfig.kt
new file mode 100644
index 0000000..a28eacb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/RedisConfig.kt
@@ -0,0 +1,48 @@
+package top.fatweb.oxygen.api.config
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.PropertyAccessor
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+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.serializer.Jackson2JsonRedisSerializer
+import org.springframework.data.redis.serializer.StringRedisSerializer
+
+/**
+ * Redis configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class RedisConfig {
+ @Bean
+ fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<*, *> {
+ val redisTemplate = RedisTemplate()
+ redisTemplate.connectionFactory = redisConnectionFactory
+ val stringRedisSerializer = StringRedisSerializer()
+ val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply {
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
+ activateDefaultTyping(
+ this.polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY
+ )
+ }
+ val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java)
+
+ // Use String Redis Serializer to serialize and deserialize redis key values
+ redisTemplate.keySerializer = stringRedisSerializer
+ redisTemplate.valueSerializer = anyJackson2JsonRedisSerializer
+
+ // The Hash key also uses the String Redis Serializer serialization method.
+ redisTemplate.hashKeySerializer = stringRedisSerializer
+ redisTemplate.hashValueSerializer = anyJackson2JsonRedisSerializer
+
+ redisTemplate.afterPropertiesSet()
+
+ return redisTemplate
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/SecurityConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/SecurityConfig.kt
new file mode 100644
index 0000000..c62c302
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/SecurityConfig.kt
@@ -0,0 +1,108 @@
+package top.fatweb.oxygen.api.config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
+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.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource
+import top.fatweb.oxygen.api.filter.JwtAuthenticationTokenFilter
+import top.fatweb.oxygen.api.handler.JwtAccessDeniedHandler
+import top.fatweb.oxygen.api.handler.JwtAuthenticationEntryPointHandler
+
+/**
+ * Spring Security configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see JwtAuthenticationTokenFilter
+ * @see JwtAuthenticationEntryPointHandler
+ * @see JwtAccessDeniedHandler
+ */
+@Configuration
+@EnableMethodSecurity
+class SecurityConfig(
+ private val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter,
+ private val authenticationEntryPointHandler: JwtAuthenticationEntryPointHandler,
+ private val accessDeniedHandler: JwtAccessDeniedHandler
+) {
+ @Bean
+ fun passwordEncoder() = BCryptPasswordEncoder()
+
+ @Bean
+ fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager =
+ authenticationConfiguration.authenticationManager
+
+ @Bean
+ fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
+ val corsConfiguration = CorsConfiguration()
+ corsConfiguration.allowedMethods = listOf("*")
+ corsConfiguration.allowedHeaders = listOf("*")
+ corsConfiguration.maxAge = 3600L
+ corsConfiguration.allowedOrigins = listOf("*")
+ val source = UrlBasedCorsConfigurationSource()
+ source.registerCorsConfiguration("/**", corsConfiguration)
+
+ return source
+ }
+
+ @Bean
+ fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity
+ // Disable CSRF
+ .csrf {
+ it.disable()
+ }
+ // Do not get SecurityContent by Session
+ .sessionManagement {
+ it.sessionCreationPolicy(
+ SessionCreationPolicy.STATELESS
+ )
+ }
+ .authorizeHttpRequests {
+ it
+ // Allow anonymous access
+ .requestMatchers(
+ "/error/thrown",
+ "/doc.html",
+ "/swagger-ui/**",
+ "/webjars/**",
+ "/v3/**",
+ "/swagger-ui.html",
+ "/favicon.ico",
+ "/login",
+ "/register",
+ "/forget",
+ "/retrieve"
+ ).anonymous()
+ .requestMatchers("/tool/detail/**", "/tool/store", "/tool/store/*", "/system/user/info/*").permitAll()
+ // Authentication required
+ .anyRequest().authenticated()
+ }
+
+ .logout {
+ it.disable()
+ }
+
+ .exceptionHandling {
+ it.authenticationEntryPoint(
+ authenticationEntryPointHandler
+ )
+ it.accessDeniedHandler(
+ accessDeniedHandler
+ )
+ }
+
+ .cors {
+ it.configurationSource(
+ corsConfigurationSource()
+ )
+ }
+
+ .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java).build()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/SwaggerConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/SwaggerConfig.kt
new file mode 100644
index 0000000..af0ca61
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/SwaggerConfig.kt
@@ -0,0 +1,28 @@
+package top.fatweb.oxygen.api.config
+
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Contact
+import io.swagger.v3.oas.models.info.Info
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import top.fatweb.oxygen.api.properties.ServerProperties
+
+/**
+ * Swagger API doc configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class SwaggerConfig {
+ @Bean
+ fun customOpenAPI(): OpenAPI? {
+ val contact = Contact().name("FatttSnake").url("https://fatweb.top").email("fatttsnake@gmail.com")
+ return OpenAPI().info(
+ Info().title("Oxygen API 文档").description("Oxygen 后端 API 文档,包含各个 Controller 调用信息")
+ .contact(contact).version(
+ ServerProperties.version
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/SysLogConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/SysLogConfig.kt
new file mode 100644
index 0000000..f7a29b8
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/SysLogConfig.kt
@@ -0,0 +1,24 @@
+package top.fatweb.oxygen.api.config
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
+import top.fatweb.oxygen.api.aop.SysLogInterceptor
+
+/**
+ * System log configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SysLogInterceptor
+ * @see WebMvcConfigurer
+ */
+@Configuration
+class SysLogConfig(
+ private val sysLogInterceptor: SysLogInterceptor
+) : WebMvcConfigurer {
+ override fun addInterceptors(registry: InterceptorRegistry) {
+ registry.addInterceptor(sysLogInterceptor).addPathPatterns("/**")
+ .excludePathPatterns("/error/thrown", "/webjars/**")
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/VelocityEngineConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/VelocityEngineConfig.kt
new file mode 100644
index 0000000..0995ada
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/VelocityEngineConfig.kt
@@ -0,0 +1,23 @@
+package top.fatweb.oxygen.api.config
+
+import org.apache.velocity.app.VelocityEngine
+import org.apache.velocity.runtime.RuntimeConstants
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+/**
+ * Velocity engine configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Configuration
+class VelocityEngineConfig {
+ @Bean
+ fun velocityEngine() = VelocityEngine().apply {
+ setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath")
+ setProperty("classpath.resource.loader.class", ClasspathResourceLoader::class.java.name)
+ init()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcConfigurerConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcConfigurerConfig.kt
new file mode 100644
index 0000000..604a8ab
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcConfigurerConfig.kt
@@ -0,0 +1,21 @@
+package top.fatweb.oxygen.api.config
+
+import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
+import top.fatweb.oxygen.api.annotation.ApiController
+
+/**
+ * Web MVC configurer configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see WebMvcRegistrations
+ */
+@Configuration
+class WebMvcConfigurerConfig : WebMvcConfigurer {
+ override fun configurePathMatch(configurer: PathMatchConfigurer) {
+ configurer.addPathPrefix("/api/{API_VERSION}") { it.isAnnotationPresent(ApiController::class.java) }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcRegistrationsConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcRegistrationsConfig.kt
new file mode 100644
index 0000000..f65a30c
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/WebMvcRegistrationsConfig.kt
@@ -0,0 +1,18 @@
+package top.fatweb.oxygen.api.config
+
+import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
+import top.fatweb.oxygen.api.util.ApiResponseMappingHandlerMapping
+
+/**
+ * Web MVC registrations configuration
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see WebMvcRegistrations
+ */
+@Configuration
+class WebMvcRegistrationsConfig : WebMvcRegistrations {
+ override fun getRequestMappingHandlerMapping(): RequestMappingHandlerMapping = ApiResponseMappingHandlerMapping()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/ExceptionController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/ExceptionController.kt
new file mode 100644
index 0000000..c61cceb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/ExceptionController.kt
@@ -0,0 +1,19 @@
+package top.fatweb.oxygen.api.controller
+
+import jakarta.servlet.http.HttpServletRequest
+import org.springframework.web.bind.annotation.RequestMapping
+import top.fatweb.oxygen.api.annotation.HiddenController
+
+/**
+ * Exception controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@HiddenController(["/error"])
+class ExceptionController {
+ @RequestMapping("/thrown")
+ fun thrown(request: HttpServletRequest) {
+ throw request.getAttribute("filter.error") as RuntimeException
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/api/v1/AvatarController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/api/v1/AvatarController.kt
new file mode 100644
index 0000000..0747aff
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/api/v1/AvatarController.kt
@@ -0,0 +1,204 @@
+package top.fatweb.oxygen.api.controller.api.v1
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.http.MediaType
+import org.springframework.web.bind.annotation.GetMapping
+import top.fatweb.oxygen.api.annotation.ApiController
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.api.v1.avatar.AvatarBaseParam
+import top.fatweb.oxygen.api.param.api.v1.avatar.AvatarGitHubParam
+import top.fatweb.oxygen.api.service.api.v1.IAvatarService
+import top.fatweb.oxygen.api.vo.api.v1.avatar.AvatarBase64Vo
+
+/**
+ * Avatar controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IAvatarService
+ */
+@ApiController(value = "avatarControllerV1", path = ["/avatar"], name = "随机头像 V1", description = "随机头像相关接口")
+class AvatarController(
+ private val avatarService: IAvatarService
+) {
+ /**
+ * Get random avatar
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Avatar byte array
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ByteArray
+ */
+ @Operation(summary = "获取随机头像")
+ @GetMapping(produces = [MediaType.IMAGE_PNG_VALUE])
+ fun getRandom(@Valid avatarBaseParam: AvatarBaseParam?): ByteArray =
+ avatarService.random(avatarBaseParam)
+
+ /**
+ * Get random avatar as base64
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Response object includes avatar base64 string
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ResponseResult
+ * @see AvatarBase64Vo
+ */
+ @Operation(summary = "获取随机头像 Base64")
+ @GetMapping("base64")
+ fun getRandomBase64(
+ @Valid avatarBaseParam: AvatarBaseParam?
+ ): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.API_AVATAR_SUCCESS, data = avatarService.randomBase64(avatarBaseParam)
+ )
+
+ /**
+ * Get triangle avatar
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Avatar byte array
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ByteArray
+ */
+ @Operation(summary = "三角形头像")
+ @GetMapping("/triangle", produces = [MediaType.IMAGE_PNG_VALUE])
+ fun triangle(@Valid avatarBaseParam: AvatarBaseParam?): ByteArray =
+ avatarService.triangle(avatarBaseParam)
+
+ /**
+ * Get triangle avatar as base64
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Response object includes avatar base64 string
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ResponseResult
+ * @see AvatarBase64Vo
+ */
+ @Operation(summary = "三角形头像 Base64")
+ @GetMapping("/triangle/base64")
+ fun triangleBase64(
+ @Valid avatarBaseParam: AvatarBaseParam?
+ ): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.API_AVATAR_SUCCESS,
+ data = avatarService.triangleBase64(avatarBaseParam)
+ )
+
+ /**
+ * Get square avatar
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Avatar byte array
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ByteArray
+ */
+ @Operation(summary = "正方形头像")
+ @GetMapping("/square", produces = [MediaType.IMAGE_PNG_VALUE])
+ fun square(@Valid avatarBaseParam: AvatarBaseParam?): ByteArray =
+ avatarService.square(avatarBaseParam)
+
+ /**
+ * Get square avatar as base64
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Response object includes avatar base64 string
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ResponseResult
+ * @see AvatarBase64Vo
+ */
+ @Operation(summary = "正方形头像 Base64")
+ @GetMapping("/square/base64")
+ fun squareBase64(
+ @Valid avatarBaseParam: AvatarBaseParam?
+ ): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.API_AVATAR_SUCCESS,
+ data = avatarService.squareBase64(avatarBaseParam)
+ )
+
+ /**
+ * Get identicon avatar
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Avatar byte array
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ByteArray
+ */
+ @Operation(summary = "Identicon 头像")
+ @GetMapping("/identicon", produces = [MediaType.IMAGE_PNG_VALUE])
+ fun identicon(@Valid avatarBaseParam: AvatarBaseParam?): ByteArray =
+ avatarService.identicon(avatarBaseParam)
+
+ /**
+ * Get identicon avatar as base64
+ *
+ * @param avatarBaseParam Avatar base parameters
+ * @return Response object includes avatar base64 string
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarBaseParam
+ * @see ResponseResult
+ * @see AvatarBase64Vo
+ */
+ @Operation(summary = "Identicon 头像 Base64")
+ @GetMapping("/identicon/base64")
+ fun identiconBase64(
+ @Valid avatarBaseParam: AvatarBaseParam?
+ ): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.API_AVATAR_SUCCESS,
+ data = avatarService.identiconBase64(avatarBaseParam)
+ )
+
+ /**
+ * Get GitHub avatar
+ *
+ * @param avatarGitHubParam Avatar base parameters
+ * @return Avatar byte array
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarGitHubParam
+ * @see ByteArray
+ */
+ @Operation(summary = "GitHub 头像")
+ @GetMapping("/github", produces = [MediaType.IMAGE_PNG_VALUE])
+ fun github(@Valid avatarGitHubParam: AvatarGitHubParam?): ByteArray =
+ avatarService.github(avatarGitHubParam)
+
+ /**
+ * Get GitHub avatar as base64
+ *
+ * @param avatarGitHubParam Avatar base parameters
+ * @return Response object includes avatar base64 string
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AvatarGitHubParam
+ * @see ResponseResult
+ * @see AvatarBase64Vo
+ */
+ @Operation(summary = "GitHub 头像 Base64")
+ @GetMapping("/github/base64")
+ fun githubBase64(
+ @Valid avatarGitHubParam: AvatarGitHubParam?
+ ): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.API_AVATAR_SUCCESS,
+ data = avatarService.githubBase64(avatarGitHubParam)
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt
new file mode 100644
index 0000000..9808fba
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt
@@ -0,0 +1,233 @@
+package top.fatweb.oxygen.api.controller.permission
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.validation.Valid
+import org.springframework.web.bind.annotation.DeleteMapping
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.permission.*
+import top.fatweb.oxygen.api.service.permission.IAuthenticationService
+import top.fatweb.oxygen.api.util.WebUtil
+import top.fatweb.oxygen.api.vo.permission.LoginVo
+import top.fatweb.oxygen.api.vo.permission.RegisterVo
+import top.fatweb.oxygen.api.vo.permission.TokenVo
+import top.fatweb.oxygen.api.vo.permission.TwoFactorVo
+
+/**
+ * Authentication controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IAuthenticationService
+ */
+@BaseController(name = "身份认证", description = "身份认证相关接口")
+class AuthenticationController(
+ private val authenticationService: IAuthenticationService
+) {
+ /**
+ * Register
+ *
+ * @param registerParam Register parameters
+ * @return Response object includes user ID
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RegisterParam
+ * @see ResponseResult
+ * @see RegisterVo
+ */
+ @Trim
+ @Operation(summary = "注册")
+ @PostMapping("/register")
+ fun register(
+ request: HttpServletRequest,
+ @Valid @RequestBody registerParam: RegisterParam
+ ): ResponseResult = ResponseResult.success(
+ ResponseCode.PERMISSION_REGISTER_SUCCESS,
+ data = authenticationService.register(request, registerParam)
+ )
+
+
+ /**
+ * Send verify email
+ *
+ * @return Response object includes resend result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "发送验证邮件")
+ @PostMapping("/resend")
+ fun resend(): ResponseResult {
+ authenticationService.resend()
+
+ return ResponseResult.success(ResponseCode.PERMISSION_RESEND_SUCCESS)
+ }
+
+ /**
+ * Verify email
+ *
+ * @param verifyParam Verify parameters
+ * @return Response object includes verify result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see VerifyParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "验证邮箱")
+ @PostMapping("/verify")
+ fun verify(@Valid @RequestBody verifyParam: VerifyParam): ResponseResult {
+ authenticationService.verify(verifyParam)
+
+ return ResponseResult.success(ResponseCode.PERMISSION_VERIFY_SUCCESS)
+ }
+
+ /**
+ * Forget password
+ *
+ * @param request
+ * @param forgetParam Forget parameters
+ * @return Response object includes forget result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HttpServletRequest
+ * @see ForgetParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "忘记密码")
+ @PostMapping("/forget")
+ fun forget(request: HttpServletRequest, @Valid @RequestBody forgetParam: ForgetParam): ResponseResult {
+ authenticationService.forget(request, forgetParam)
+
+ return ResponseResult.success(ResponseCode.PERMISSION_FORGET_SUCCESS)
+ }
+
+ /**
+ * Retrieve password
+ *
+ * @param request
+ * @param retrieveParam Retrieve parameters
+ * @return Response object includes retrieve result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HttpServletRequest
+ * @see RetrieveParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "找回密码")
+ @PostMapping("/retrieve")
+ fun retrieve(
+ request: HttpServletRequest,
+ @Valid @RequestBody retrieveParam: RetrieveParam
+ ): ResponseResult {
+ authenticationService.retrieve(request, retrieveParam)
+
+ return ResponseResult.success(ResponseCode.PERMISSION_RETRIEVE_SUCCESS)
+ }
+
+ /**
+ * Login
+ *
+ * @param request
+ * @param loginParam Login parameters
+ * @return Response object includes login result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HttpServletRequest
+ * @see LoginParam
+ * @see ResponseResult
+ * @see LoginVo
+ */
+ @Trim
+ @Operation(summary = "登录")
+ @PostMapping("/login")
+ fun login(request: HttpServletRequest, @Valid @RequestBody loginParam: LoginParam): ResponseResult =
+ ResponseResult.success(
+ ResponseCode.PERMISSION_LOGIN_SUCCESS,
+ "Login success",
+ authenticationService.login(request, loginParam)
+ )
+
+ /**
+ * Create two-factor
+ *
+ * @return Response object includes two-factor QR code
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see TwoFactorVo
+ */
+ @Operation(summary = "创建双因素验证码")
+ @GetMapping("/two-factor")
+ fun createTwoFactor(): ResponseResult =
+ ResponseResult.success(data = authenticationService.createTwoFactor())
+
+ /**
+ * Validate two-factor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Operation(summary = "验证双因素")
+ @PostMapping("/two-factor")
+ fun validateTwoFactor(@RequestBody @Valid twoFactorValidateParam: TwoFactorValidateParam): ResponseResult =
+ if (authenticationService.validateTwoFactor(twoFactorValidateParam)) ResponseResult.success()
+ else ResponseResult.fail()
+
+ /**
+ * Remove two-factor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Operation(summary = "移除双因素")
+ @DeleteMapping("/two-factor")
+ fun removeTwoFactor(@RequestBody @Valid twoFactorRemoveParam: TwoFactorRemoveParam): ResponseResult =
+ if (authenticationService.removeTwoFactor(twoFactorRemoveParam)) ResponseResult.success()
+ else ResponseResult.fail()
+
+
+ /**
+ * Logout
+ *
+ * @param request
+ * @return Response object includes logout result
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HttpServletRequest
+ * @see ResponseResult
+ */
+ @Operation(summary = "登出")
+ @PostMapping("/logout")
+ fun logout(request: HttpServletRequest): ResponseResult =
+ when (authenticationService.logout(WebUtil.getToken(request))) {
+ true -> ResponseResult.success(ResponseCode.PERMISSION_LOGOUT_SUCCESS, "Logout success", null)
+ false -> ResponseResult.fail(ResponseCode.PERMISSION_LOGOUT_FAILED, "Logout failed", null)
+ }
+
+ /**
+ * Renew token
+ *
+ * @param request
+ * @return Response object includes new token
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see HttpServletRequest
+ * @see ResponseResult
+ * @see TokenVo
+ */
+ @Operation(summary = "更新 Token")
+ @GetMapping("/token")
+ fun renewToken(request: HttpServletRequest): ResponseResult = ResponseResult.success(
+ ResponseCode.PERMISSION_TOKEN_RENEW_SUCCESS,
+ "Token renew success",
+ authenticationService.renewToken(WebUtil.getToken(request))
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/GroupController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/GroupController.kt
new file mode 100644
index 0000000..549b4c8
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/GroupController.kt
@@ -0,0 +1,174 @@
+package top.fatweb.oxygen.api.controller.permission
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.permission.group.*
+import top.fatweb.oxygen.api.service.permission.IGroupService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.GroupWithRoleVo
+import top.fatweb.oxygen.api.vo.permission.base.GroupVo
+
+/**
+ * Group management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IGroupService
+ */
+@BaseController(path = ["/system/group"], name = "用户组管理", description = "用户组管理相关接口")
+class GroupController(
+ val groupService: IGroupService
+) {
+ /**
+ * Get group by ID
+ *
+ * @param id Group ID
+ * @return Response object includes group information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see GroupWithRoleVo
+ */
+ @Operation(summary = "获取单个用户组")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:group:query:one')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = groupService.getOne(id))
+
+ /**
+ * Get group paging information
+ *
+ * @param groupGetParam Get group parameters
+ * @return Response object includes group paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupGetParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see GroupWithRoleVo
+ */
+ @Trim
+ @Operation(summary = "获取用户组")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:group:query:all')")
+ fun get(@Valid groupGetParam: GroupGetParam?): ResponseResult> =
+ ResponseResult.databaseSuccess(
+ data = groupService.getPage(groupGetParam)
+ )
+
+ /**
+ * Get group list
+ *
+ * @return Response object includes group list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see GroupVo
+ */
+ @Operation(summary = "获取用户组列表")
+ @GetMapping("/list")
+ @PreAuthorize("hasAnyAuthority('system:group:query:list', 'system:user:add:one', 'system:user:modify:one')")
+ fun list(): ResponseResult> =
+ ResponseResult.databaseSuccess(
+ data = groupService.getList()
+ )
+
+ /**
+ * Add group
+ *
+ * @param groupAddParam Add group parameters
+ * @return Response object includes group information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupAddParam
+ * @see ResponseResult
+ * @see GroupVo
+ */
+ @Trim
+ @Operation(summary = "添加用户组")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:group:add:one')")
+ fun add(@Valid @RequestBody groupAddParam: GroupAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS, data = groupService.add(groupAddParam)
+ )
+
+ /**
+ * Update group
+ *
+ * @param groupUpdateParam Update group parameters
+ * @return Response object includes group information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupUpdateParam
+ * @see ResponseResult
+ * @see GroupVo
+ */
+ @Trim
+ @Operation(summary = "修改用户组")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:group:modify:one')")
+ fun update(@Valid @RequestBody groupUpdateParam: GroupUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS, data = groupService.update(groupUpdateParam)
+ )
+
+ /**
+ * Update status of group
+ *
+ * @param groupUpdateStatusParam Update status of group parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupUpdateStatusParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "修改用户组状态")
+ @PatchMapping
+ @PreAuthorize("hasAnyAuthority('system:group:modify:status')")
+ fun updateStatus(@Valid @RequestBody groupUpdateStatusParam: GroupUpdateStatusParam): ResponseResult {
+ groupService.status(groupUpdateStatusParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ }
+
+ /**
+ * Delete group by ID
+ *
+ * @param id Group ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除用户组")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:group:delete:one')")
+ fun delete(@PathVariable id: Long): ResponseResult {
+ groupService.deleteOne(id)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+
+ /**
+ * Delete group by list
+ *
+ * @param groupDeleteParam Delete group parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupDeleteParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "批量删除用户组")
+ @DeleteMapping
+ @PreAuthorize("hasAnyAuthority('system:group:delete:multiple')")
+ fun deleteList(@Valid @RequestBody groupDeleteParam: GroupDeleteParam): ResponseResult {
+ groupService.delete(groupDeleteParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/PowerController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/PowerController.kt
new file mode 100644
index 0000000..f4bbd0d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/PowerController.kt
@@ -0,0 +1,35 @@
+package top.fatweb.oxygen.api.controller.permission
+
+import io.swagger.v3.oas.annotations.Operation
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.GetMapping
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.service.permission.IPowerService
+import top.fatweb.oxygen.api.vo.permission.PowerSetVo
+
+/**
+ * Power management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPowerService
+ */
+@BaseController(path = ["/system/power"], name = "权限管理", description = "权限管理相关接口")
+class PowerController(
+ private val powerService: IPowerService
+) {
+ /**
+ * Get power list
+ *
+ * @return Response object includes power list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see PowerSetVo
+ */
+ @Operation(summary = "获取权限列表")
+ @GetMapping("/list")
+ @PreAuthorize("hasAnyAuthority('system:power:query:list', 'system:role:add:one', 'system:role:modify:one')")
+ fun getList(): ResponseResult = ResponseResult.databaseSuccess(data = powerService.getList())
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/RoleController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/RoleController.kt
new file mode 100644
index 0000000..71c16a1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/RoleController.kt
@@ -0,0 +1,174 @@
+package top.fatweb.oxygen.api.controller.permission
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.permission.role.*
+import top.fatweb.oxygen.api.service.permission.IRoleService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.RoleWithPowerVo
+import top.fatweb.oxygen.api.vo.permission.base.RoleVo
+
+/**
+ * Role management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IRoleService
+ */
+@BaseController(path = ["/system/role"], name = "角色管理", description = "角色管理相关接口")
+class RoleController(
+ private val roleService: IRoleService
+) {
+ /**
+ * Get role by ID
+ *
+ * @param id Role ID
+ * @return Response object includes role information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see RoleWithPowerVo
+ */
+ @Operation(summary = "获取单个角色")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:role:query:one')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = roleService.getOne(id))
+
+ /**
+ * Get role paging information
+ *
+ * @param roleGetParam Get role parameters
+ * @return Response object includes role paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleGetParam
+ * @see ResponseResult
+ * @see RoleWithPowerVo
+ */
+ @Trim
+ @Operation(summary = "获取角色")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:role:query:all')")
+ fun get(roleGetParam: RoleGetParam?): ResponseResult> =
+ ResponseResult.databaseSuccess(
+ data = roleService.getPage(roleGetParam)
+ )
+
+ /**
+ * Get role list
+ *
+ * @return Response object includes role list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see RoleVo
+ */
+ @Operation(summary = "获取角色列表")
+ @GetMapping("/list")
+ @PreAuthorize("hasAnyAuthority('system:role:query:list', 'system:group:add:one', 'system:group:modify:one', 'system:user:add:one', 'system:user:modify:one')")
+ fun list(): ResponseResult> {
+ return ResponseResult.databaseSuccess(
+ data = roleService.getList()
+ )
+ }
+
+ /**
+ * Add role
+ *
+ * @param roleAddParam Add role parameters
+ * @return Response object includes role information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleAddParam
+ * @see ResponseResult
+ * @see RoleVo
+ */
+ @Trim
+ @Operation(summary = "添加角色")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:role:add:one')")
+ fun add(@Valid @RequestBody roleAddParam: RoleAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS, data = roleService.add(roleAddParam)
+ )
+
+ /**
+ * Update role
+ *
+ * @param roleUpdateParam Update role parameters
+ * @return Response object includes role information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleUpdateParam
+ * @see ResponseResult
+ * @see RoleVo
+ */
+ @Trim
+ @Operation(summary = "修改角色")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:role:modify:one')")
+ fun update(@Valid @RequestBody roleUpdateParam: RoleUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS, data = roleService.update(roleUpdateParam)
+ )
+
+ /**
+ * Update status of role
+ *
+ * @param roleUpdateStatusParam Update status of role parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleUpdateStatusParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "修改角色状态")
+ @PatchMapping
+ @PreAuthorize("hasAnyAuthority('system:role:modify:status')")
+ fun status(@Valid @RequestBody roleUpdateStatusParam: RoleUpdateStatusParam): ResponseResult {
+ roleService.status(roleUpdateStatusParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ }
+
+ /**
+ * Delete role by ID
+ *
+ * @param id
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除角色")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:role:delete:one')")
+ fun delete(@PathVariable id: Long): ResponseResult {
+ roleService.deleteOne(id)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+
+ /**
+ * Delete role by list
+ *
+ * @param roleDeleteParam Delete role parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleDeleteParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "批量删除角色")
+ @DeleteMapping
+ @PreAuthorize("hasAnyAuthority('system:role:delete:multiple')")
+ fun deleteList(@Valid @RequestBody roleDeleteParam: RoleDeleteParam): ResponseResult {
+ roleService.delete(roleDeleteParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/UserController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/UserController.kt
new file mode 100644
index 0000000..ca9b504
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/UserController.kt
@@ -0,0 +1,223 @@
+package top.fatweb.oxygen.api.controller.permission
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.permission.user.*
+import top.fatweb.oxygen.api.service.permission.IUserService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.UserWithInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithPasswordRoleInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithPowerInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithRoleInfoVo
+
+/**
+ * User management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IUserService
+ */
+@BaseController(path = ["/system/user"], name = "用户管理", description = "用户管理相关接口")
+class UserController(
+ private val userService: IUserService
+) {
+ /**
+ * Get current user information
+ *
+ * @return Response object includes user information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see UserWithPowerInfoVo
+ */
+ @Operation(summary = "获取当前用户信息")
+ @GetMapping("/info")
+ fun getInfo(): ResponseResult =
+ ResponseResult.databaseSuccess(data = userService.getInfo())
+
+ /**
+ * Get basic user information
+ *
+ * @param username Username
+ * @return Response object includes user basic information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see UserWithPowerInfoVo
+ */
+ @Trim
+ @Operation(summary = "获取指定用户基本信息")
+ @GetMapping("/info/{username}")
+ fun getBasicInfo(@PathVariable username: String): ResponseResult =
+ ResponseResult.databaseSuccess(data = userService.getBasicInfo(username.trim()))
+
+ /**
+ * Update current user information
+ *
+ * @param userInfoUpdateParam Update user information parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserInfoUpdateParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "更新当前用户信息")
+ @PatchMapping("info")
+ fun updateInfo(@RequestBody @Valid userInfoUpdateParam: UserInfoUpdateParam): ResponseResult =
+ if (userService.updateInfo(userInfoUpdateParam)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ else ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_FAILED)
+
+ /**
+ * Change password
+ *
+ * @param userChangePasswordParam User change password parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserChangePasswordParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "更改密码")
+ @PostMapping("info")
+ fun password(@RequestBody @Valid userChangePasswordParam: UserChangePasswordParam): ResponseResult {
+ userService.password(userChangePasswordParam)
+
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ }
+
+ /**
+ * Get user by ID
+ *
+ * @param id User ID
+ * @return Response object includes user information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see UserWithRoleInfoVo
+ */
+ @Operation(summary = "获取单个用户")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:user:query:one')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = userService.getOne(id))
+
+ /**
+ * Get user paging information
+ *
+ * @param userGetParam Get user parameters
+ * @return Response object includes user paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserGetParam
+ * @see ResponseResult
+ * @see UserWithRoleInfoVo
+ */
+ @Trim
+ @Operation(summary = "获取用户")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:user:query:all')")
+ fun get(@Valid userGetParam: UserGetParam?): ResponseResult> =
+ ResponseResult.databaseSuccess(
+ data = userService.getPage(userGetParam)
+ )
+
+ /**
+ * Add user
+ *
+ * @param userAddParam Add user parameters
+ * @return Response object includes user information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserAddParam
+ * @see ResponseResult
+ * @see UserWithPasswordRoleInfoVo
+ */
+ @Trim
+ @Operation(summary = "添加用户")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:user:add:one')")
+ fun add(@Valid @RequestBody userAddParam: UserAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS, data = userService.add(userAddParam)
+ )
+
+ /**
+ * Update user
+ *
+ * @param userUpdateParam Update user parameters
+ * @return Response object includes user information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserUpdateParam
+ * @see ResponseResult
+ * @see UserWithRoleInfoVo
+ */
+ @Trim
+ @Operation(summary = "修改用户")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:user:modify:one')")
+ fun update(@Valid @RequestBody userUpdateParam: UserUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS, data = userService.update(userUpdateParam)
+ )
+
+ /**
+ * Update user password
+ *
+ * @param userUpdatePasswordParam Update user password parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserUpdatePasswordParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "修改密码")
+ @PatchMapping
+ @PreAuthorize("hasAnyAuthority('system:user:modify:password')")
+ fun password(@Valid @RequestBody userUpdatePasswordParam: UserUpdatePasswordParam): ResponseResult {
+ userService.password(userUpdatePasswordParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ }
+
+ /**
+ * Delete user by ID
+ *
+ * @param id User ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除用户")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:user:delete:one')")
+ fun delete(@PathVariable id: Long): ResponseResult {
+ userService.deleteOne(id)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+
+ /**
+ * Delete user by list
+ *
+ * @param userDeleteParam Delete user parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserDeleteParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "批量删除用户")
+ @DeleteMapping
+ @PreAuthorize("hasAnyAuthority('system:user:delete:multiple')")
+ fun deleteList(@Valid @RequestBody userDeleteParam: UserDeleteParam): ResponseResult {
+ userService.delete(userDeleteParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt
new file mode 100644
index 0000000..c41a6a0
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt
@@ -0,0 +1,221 @@
+package top.fatweb.oxygen.api.controller.system
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.system.*
+import top.fatweb.oxygen.api.service.system.ISensitiveWordService
+import top.fatweb.oxygen.api.service.system.ISettingsService
+import top.fatweb.oxygen.api.vo.system.BaseSettingsVo
+import top.fatweb.oxygen.api.vo.system.MailSettingsVo
+import top.fatweb.oxygen.api.vo.system.SensitiveWordVo
+import top.fatweb.oxygen.api.vo.system.TwoFactorSettingsVo
+
+/**
+ * System settings management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ISettingsService
+ * @see ISensitiveWordService
+ */
+@BaseController(path = ["/system/settings"], name = "系统设置", description = "系统设置相关接口")
+class SettingsController(
+ private val settingsService: ISettingsService,
+ private val sensitiveWordService: ISensitiveWordService
+) {
+ /**
+ * Get base settings
+ *
+ * @return Response object includes base settings information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see BaseSettingsVo
+ */
+ @Operation(summary = "获取基础设置")
+ @GetMapping("/base")
+ @PreAuthorize("hasAnyAuthority('system:settings:query:base')")
+ fun getApp(): ResponseResult =
+ ResponseResult.success(data = settingsService.getBase())
+
+ /**
+ * Update base settings
+ *
+ * @param baseSettingsParam Base settings parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseSettingsParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "更新基础设置")
+ @PutMapping("/base")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:base')")
+ fun updateApp(@RequestBody baseSettingsParam: BaseSettingsParam): ResponseResult {
+ settingsService.updateBase(baseSettingsParam)
+ return ResponseResult.success()
+ }
+
+ /**
+ * Get mail settings
+ *
+ * @return Response object includes mail settings
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see MailSettingsVo
+ */
+ @Operation(summary = "获取邮件设置")
+ @GetMapping("/mail")
+ @PreAuthorize("hasAnyAuthority('system:settings:query:mail')")
+ fun getMail(): ResponseResult =
+ ResponseResult.success(data = settingsService.getMail())
+
+ /**
+ * Update mail settings
+ *
+ * @param mailSettingsParam Mail settings parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see MailSettingsParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "更新邮件设置")
+ @PutMapping("/mail")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:mail')")
+ fun updateMail(@RequestBody mailSettingsParam: MailSettingsParam): ResponseResult {
+ settingsService.updateMail(mailSettingsParam)
+ return ResponseResult.success()
+ }
+
+ /**
+ * Send mail test
+ *
+ * @param mailSendParam Mail send parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see MailSendParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "邮件发送测试")
+ @PostMapping("/mail")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:mail')")
+ fun sendMail(@RequestBody @Valid mailSendParam: MailSendParam): ResponseResult {
+ settingsService.sendMail(mailSendParam)
+ return ResponseResult.success()
+ }
+
+ /**
+ * Get sensitive word settings
+ *
+ * @return Response object includes sensitive word settings information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see SensitiveWordVo
+ */
+ @Operation(summary = "获取敏感词配置")
+ @GetMapping("/sensitive")
+ @PreAuthorize("hasAnyAuthority('system:settings:query:sensitive')")
+ fun getSensitive(): ResponseResult> =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_SELECT_SUCCESS, data = sensitiveWordService.get())
+
+ /**
+ * Add sensitive word
+ *
+ * @param sensitiveWordAddParam Add sensitive word settings parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SensitiveWordAddParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "添加敏感词")
+ @PostMapping("/sensitive")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:sensitive')")
+ fun addSensitive(@RequestBody @Valid sensitiveWordAddParam: SensitiveWordAddParam): ResponseResult {
+ sensitiveWordService.add(sensitiveWordAddParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS)
+ }
+
+ /**
+ * Update sensitive word
+ *
+ * @param sensitiveWordUpdateParam Update sensitive word settings parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SensitiveWordUpdateParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "修改敏感词")
+ @PutMapping("/sensitive")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:sensitive')")
+ fun updateSensitive(@RequestBody sensitiveWordUpdateParam: SensitiveWordUpdateParam): ResponseResult {
+ sensitiveWordService.update(sensitiveWordUpdateParam)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS)
+ }
+
+ /**
+ * Delete sensitive word
+ *
+ * @see id Sensitive word ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除敏感词")
+ @DeleteMapping("/sensitive/{id}")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:sensitive')")
+ fun deleteSensitive(@PathVariable id: Long): ResponseResult {
+ sensitiveWordService.delete(id)
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+
+ /**
+ * Get two-factor settings
+ *
+ * @return Response object includes two-factor settings information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see TwoFactorSettingsVo
+ */
+ @Operation(summary = "获取双因素设置")
+ @GetMapping("/two-factor")
+ @PreAuthorize("hasAnyAuthority('system:settings:query:two-factor')")
+ fun getTwoFactor(): ResponseResult =
+ ResponseResult.success(data = settingsService.getTwoFactor())
+
+ /**
+ * Update two-factor settings
+ *
+ * @param twoFactorSettingsParam Two-factor settings parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see TwoFactorSettingsParam
+ * @see ResponseResult
+ */
+ @Trim
+ @Operation(summary = "更新双因素设置")
+ @PutMapping("/two-factor")
+ @PreAuthorize("hasAnyAuthority('system:settings:modify:two-factor')")
+ fun updateTwoFactor(@RequestBody twoFactorSettingsParam: TwoFactorSettingsParam): ResponseResult {
+ settingsService.updateTwoFactor(twoFactorSettingsParam)
+ return ResponseResult.success()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/system/StatisticsController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/StatisticsController.kt
new file mode 100644
index 0000000..a04d961
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/StatisticsController.kt
@@ -0,0 +1,113 @@
+package top.fatweb.oxygen.api.controller.system
+
+import io.swagger.v3.oas.annotations.Operation
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.GetMapping
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.system.ActiveInfoGetParam
+import top.fatweb.oxygen.api.param.system.OnlineInfoGetParam
+import top.fatweb.oxygen.api.service.system.IStatisticsService
+import top.fatweb.oxygen.api.vo.system.*
+
+/**
+ * Statistics management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IStatisticsService
+ */
+@BaseController(path = ["/system/statistics"], name = "统计接口", description = "系统信息统计相关接口")
+class StatisticsController(
+ private val statisticService: IStatisticsService
+) {
+ /**
+ * Get software information
+ *
+ * @return Response object includes software information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see SoftwareInfoVo
+ */
+ @Operation(summary = "获取软件信息")
+ @GetMapping("/software")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:base')")
+ fun software(): ResponseResult = ResponseResult.success(data = statisticService.software())
+
+ /**
+ * Get hardware information
+ *
+ * @return Response object includes hardware information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see HardwareInfoVo
+ */
+ @Operation(summary = "获取硬件信息")
+ @GetMapping("/hardware")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:base')")
+ fun hardware(): ResponseResult = ResponseResult.success(data = statisticService.hardware())
+
+ /**
+ * Get CPU information
+ *
+ * @return Response object includes CPU information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see CpuInfoVo
+ */
+ @Operation(summary = "获取 CPU 信息")
+ @GetMapping("/cpu")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:real')")
+ fun cpu(): ResponseResult = ResponseResult.success(data = statisticService.cpu())
+
+ /**
+ * Get storage information
+ *
+ * @return Response object includes storage information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see StorageInfoVo
+ */
+ @Operation(summary = "获取存储信息")
+ @GetMapping("/storage")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:real')")
+ fun storage(): ResponseResult = ResponseResult.success(data = statisticService.storage())
+
+ /**
+ * Get the history of online users information
+ *
+ * @param onlineInfoGetParam Get online information parameters
+ * @return Response object includes online user information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see OnlineInfoGetParam
+ * @see ResponseResult
+ * @see OnlineInfoVo
+ */
+ @Operation(summary = "获取在线用户数量信息")
+ @GetMapping("/online")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:usage')")
+ fun online(onlineInfoGetParam: OnlineInfoGetParam?): ResponseResult =
+ ResponseResult.success(data = statisticService.online(onlineInfoGetParam))
+
+ /**
+ * Get the history of active information
+ *
+ * @param activeInfoGetParam Get active information parameters
+ * @return Response object includes history of active information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ActiveInfoGetParam
+ * @see ResponseResult
+ * @see ActiveInfoVo
+ */
+ @Operation(summary = "获取用户活跃信息")
+ @GetMapping("/active")
+ @PreAuthorize("hasAnyAuthority('system:statistics:query:usage')")
+ fun active(activeInfoGetParam: ActiveInfoGetParam): ResponseResult =
+ ResponseResult.success(data = statisticService.active(activeInfoGetParam))
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SysLogController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SysLogController.kt
new file mode 100644
index 0000000..8cc0713
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SysLogController.kt
@@ -0,0 +1,47 @@
+package top.fatweb.oxygen.api.controller.system
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.GetMapping
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.system.SysLogGetParam
+import top.fatweb.oxygen.api.service.system.ISysLogService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.system.SysLogVo
+
+/**
+ * System log viewer controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ISysLogService
+ */
+@BaseController(path = ["/system/log"], name = "系统日志", description = "系统日志相关接口")
+class SysLogController(
+ private val sysLogService: ISysLogService
+) {
+ /**
+ * Get system log in page
+ *
+ * @param sysLogGetParam Get system log parameters
+ * @return Response object includes system log in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SysLogGetParam
+ * @see ResponseResult
+ * @see SysLogVo
+ */
+ @Trim
+ @Operation(summary = "获取")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:log:query:all')")
+ fun get(@Valid sysLogGetParam: SysLogGetParam?): ResponseResult> {
+ return ResponseResult.success(
+ ResponseCode.DATABASE_SELECT_SUCCESS, data = sysLogService.getPage(sysLogGetParam)
+ )
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/BaseController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/BaseController.kt
new file mode 100644
index 0000000..8741981
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/BaseController.kt
@@ -0,0 +1,136 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.tool.ToolBaseAddParam
+import top.fatweb.oxygen.api.param.tool.ToolBaseGetParam
+import top.fatweb.oxygen.api.param.tool.ToolBaseUpdateParam
+import top.fatweb.oxygen.api.service.tool.IToolBaseService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolBaseVo
+
+/**
+ * Tool base management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IToolBaseService
+ */
+@BaseController(path = ["/system/tool/base"], name = "工具基板管理", description = "工具基板管理相关接口")
+class BaseController(
+ private val toolBaseService: IToolBaseService
+) {
+ /**
+ * Get tool base by ID
+ *
+ * @param id Tool base ID
+ * @return Response object includes tool base information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolBaseVo
+ */
+ @Operation(summary = "获取单个基板")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:query:base')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = toolBaseService.getOne(id))
+
+ /**
+ * Get tool base paging information
+ *
+ * @param toolBaseGetParam Get tool base parameters
+ * @return Response object includes tool base paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBaseGetParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolBaseVo
+ */
+ @Operation(summary = "获取基板")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:query:base')")
+ fun get(toolBaseGetParam: ToolBaseGetParam?): ResponseResult> =
+ ResponseResult.databaseSuccess(data = toolBaseService.get(toolBaseGetParam))
+
+ /**
+ * Get tool base list
+ *
+ * @return Response object includes tool base list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolBaseVo
+ */
+ @Operation(summary = "获取基板列表")
+ @GetMapping("/list")
+ @PreAuthorize("hasAnyAuthority('system:tool:add:template', 'system:tool:modify:template')")
+ fun list(): ResponseResult> =
+ ResponseResult.databaseSuccess(data = toolBaseService.getList())
+
+
+ /**
+ * Add tool base
+ *
+ * @param toolBaseAddParam Add tool base parameters
+ * @return Response object includes tool base information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBaseAddParam
+ * @see ResponseResult
+ * @see ToolBaseVo
+ */
+ @Trim
+ @Operation(summary = "新增基板")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:add:base')")
+ fun add(@RequestBody @Valid toolBaseAddParam: ToolBaseAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS,
+ data = toolBaseService.add(toolBaseAddParam)
+ )
+
+ /**
+ * Update tool base
+ *
+ * @param toolBaseUpdateParam Update tool base parameters
+ * @return Response object includes tool base information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBaseUpdateParam
+ * @see ResponseResult
+ * @see ToolBaseVo
+ */
+ @Trim
+ @Operation(summary = "更新基板")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:base')")
+ fun update(@RequestBody @Valid toolBaseUpdateParam: ToolBaseUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS,
+ data = toolBaseService.update(toolBaseUpdateParam)
+ )
+
+ /**
+ * Delete tool base by ID
+ *
+ * @param id Tool base ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除基板")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:delete:base')")
+ fun delete(@PathVariable id: Long): ResponseResult =
+ if (toolBaseService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/CategoryController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/CategoryController.kt
new file mode 100644
index 0000000..460c3d4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/CategoryController.kt
@@ -0,0 +1,115 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.tool.ToolCategoryAddParam
+import top.fatweb.oxygen.api.param.tool.ToolCategoryUpdateParam
+import top.fatweb.oxygen.api.service.tool.IToolCategoryService
+import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
+
+/**
+ * Tool category management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IToolCategoryService
+ */
+@BaseController(path = ["/system/tool/category"], name = "工具类别管理", description = "工具列别管理相关接口")
+class CategoryController(
+ private val toolCategoryService: IToolCategoryService
+) {
+ /**
+ * Get tool category by ID
+ *
+ * @param id Tool category ID
+ * @return Response object includes tool template information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolCategoryVo
+ */
+ @Operation(summary = "获取单个类别")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:query:category')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = toolCategoryService.getOne(id))
+
+ /**
+ * Get tool category list
+ *
+ * @return Response object includes tool template list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolCategoryVo
+ */
+ @Operation(summary = "获取类别")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:query:category')")
+ fun get(): ResponseResult> =
+ ResponseResult.databaseSuccess(data = toolCategoryService.get())
+
+ /**
+ * Add tool category
+ *
+ * @param toolCategoryAddParam Add tool category parameters
+ * @return Response object includes tool category information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCategoryAddParam
+ * @see ResponseResult
+ * @see ToolCategoryVo
+ */
+ @Trim
+ @Operation(summary = "新增类别")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:add:category')")
+ fun add(@RequestBody @Valid toolCategoryAddParam: ToolCategoryAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS,
+ data = toolCategoryService.add(toolCategoryAddParam)
+ )
+
+ /**
+ * Update tool category
+ *
+ * @param toolCategoryUpdateParam Update tool category parameters
+ * @return Response object includes tool category information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCategoryUpdateParam
+ * @see ResponseResult
+ * @see ToolCategoryVo
+ */
+ @Trim
+ @Operation(summary = "更新类别")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:category')")
+ fun update(@RequestBody @Valid toolCategoryUpdateParam: ToolCategoryUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS,
+ data = toolCategoryService.update(toolCategoryUpdateParam)
+ )
+
+ /**
+ * Delete tool category
+ *
+ * @param id Tool category ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除类别")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:delete:category')")
+ fun delete(@PathVariable id: Long): ResponseResult =
+ if (toolCategoryService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/EditController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/EditController.kt
new file mode 100644
index 0000000..dec2efb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/EditController.kt
@@ -0,0 +1,215 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.entity.tool.Platform
+import top.fatweb.oxygen.api.param.PageSortParam
+import top.fatweb.oxygen.api.param.tool.ToolCreateParam
+import top.fatweb.oxygen.api.param.tool.ToolUpdateParam
+import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
+import top.fatweb.oxygen.api.service.tool.IEditService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
+import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
+import top.fatweb.oxygen.api.vo.tool.ToolVo
+
+/**
+ * Tool edit controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IEditService
+ */
+@BaseController(path = ["/tool"], name = "工具编辑", description = "工具编辑相关接口")
+class EditController(
+ private val editService: IEditService
+) {
+ /**
+ * Get tool template list
+ *
+ * @return Response object includes tool template list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolTemplateVo
+ */
+ @Operation(summary = "获取模板")
+ @GetMapping("/template")
+ fun getTemplate(platform: Platform): ResponseResult> =
+ ResponseResult.databaseSuccess(data = editService.getTemplate(platform))
+
+ /**
+ * Get tool template by ID
+ *
+ * @param id ID
+ * @return Response object includes tool template information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolTemplateVo
+ */
+ @Operation(summary = "获取单个模板")
+ @GetMapping("/template/{id}")
+ fun getTemplate(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = editService.getTemplate(id))
+
+ /**
+ * Get tool category list
+ *
+ * @return Response object includes tool category list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolCategoryVo
+ */
+ @Operation(summary = "获取类别")
+ @GetMapping("/category")
+ fun getCategory(): ResponseResult> =
+ ResponseResult.databaseSuccess(data = editService.getCategory())
+
+ /**
+ * Create tool
+ *
+ * @param toolCreateParam Create tool parameters
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCreateParam
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "创建工具")
+ @PostMapping
+ fun create(@RequestBody @Valid toolCreateParam: ToolCreateParam): ResponseResult =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS, data = editService.create(toolCreateParam))
+
+ /**
+ * Upgrade tool
+ *
+ * @param toolUpgradeParam Upgrade tool parameters
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolUpgradeParam
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "升级工具")
+ @PatchMapping
+ fun upgrade(@RequestBody @Valid toolUpgradeParam: ToolUpgradeParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS,
+ data = editService.upgrade(toolUpgradeParam)
+ )
+
+ /**
+ * Get personal tool
+ *
+ * @return Response object includes tool list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see PageSortParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "获取个人工具")
+ @GetMapping
+ fun get(@Valid pageSortParam: PageSortParam): ResponseResult> =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_SELECT_SUCCESS, data = editService.getPage(pageSortParam))
+
+ /**
+ * Get tool detail
+ *
+ * @param username Username
+ * @param toolId Tool ID
+ * @param ver Version
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Operation(summary = "获取工具内容")
+ @GetMapping("/detail/{username}/{toolId}/{ver}")
+ fun detail(
+ @PathVariable username: String,
+ @PathVariable toolId: String,
+ @PathVariable ver: String,
+ platform: Platform
+ ): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_SELECT_SUCCESS,
+ data = editService.detail(username.trim(), toolId.trim(), ver.trim(), platform)
+ )
+
+ /**
+ * Update tool
+ *
+ * @param toolUpdateParam Update tool parameters
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolUpdateParam
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "更新工具")
+ @PutMapping
+ fun update(@RequestBody @Valid toolUpdateParam: ToolUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS, data = editService.update(toolUpdateParam))
+
+ /**
+ * Submit tool review
+ *
+ * @param id Tool ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "提交工具审核")
+ @PostMapping("/{id}")
+ fun submit(@PathVariable id: Long): ResponseResult =
+ if (editService.submit(id)) ResponseResult.success(ResponseCode.TOOL_SUBMIT_SUCCESS)
+ else ResponseResult.fail(ResponseCode.TOOL_SUBMIT_ERROR)
+
+ /**
+ * Cancel tool review
+ *
+ * @param id Tool ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "取消工具审核")
+ @PutMapping("/{id}")
+ fun cancel(@PathVariable id: Long): ResponseResult =
+ if (editService.cancel(id)) ResponseResult.success(ResponseCode.TOOL_CANCEL_SUCCESS)
+ else ResponseResult.fail(ResponseCode.TOOL_CANCEL_ERROR)
+
+ /**
+ * Delete tool
+ *
+ * @param id Tool ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除工具")
+ @DeleteMapping("/{id}")
+ fun delete(@PathVariable id: Long): ResponseResult =
+ if (editService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/ManagementController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/ManagementController.kt
new file mode 100644
index 0000000..bda530d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/ManagementController.kt
@@ -0,0 +1,134 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.tool.ToolManagementGetParam
+import top.fatweb.oxygen.api.param.tool.ToolManagementPassParam
+import top.fatweb.oxygen.api.service.tool.IManagementService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolVo
+
+/**
+ * Tool management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IManagementService
+ */
+@BaseController(path = ["/system/tool"], name = "工具管理", description = "工具管理相关接口")
+class ManagementController(
+ private val managementService: IManagementService
+) {
+ /**
+ * Get tool by ID
+ *
+ * @param id Tool ID
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Operation(summary = "获取单个工具")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:query:tool')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = managementService.getOne(id))
+
+ /**
+ * Get tool paging information
+ *
+ * @param toolManagementGetParam Get tool parameters in tool management
+ * @return Response object includes tool paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolManagementGetParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "获取工具")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:query:tool')")
+ fun get(toolManagementGetParam: ToolManagementGetParam): ResponseResult> =
+ ResponseResult.databaseSuccess(data = managementService.getPage(toolManagementGetParam))
+
+ /**
+ * Pass tool review
+ *
+ * @param id Tool ID
+ * @param toolManagementPassParam Pass tool parameters in tool management
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolManagementPassParam
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Operation(summary = "通过审核")
+ @PostMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:tool')")
+ fun pass(
+ @PathVariable id: Long,
+ @RequestBody @Valid toolManagementPassParam: ToolManagementPassParam
+ ): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS,
+ data = managementService.pass(id, toolManagementPassParam)
+ )
+
+ /**
+ * Reject tool review
+ *
+ * @param id Tool ID
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Operation(summary = "驳回审核")
+ @PutMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:tool')")
+ fun reject(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS, data = managementService.reject(id))
+
+ /**
+ * Put off shelve
+ *
+ * @param id Tool ID
+ * @return Response object includes tool information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolVo
+ */
+ @Operation(summary = "下架")
+ @PatchMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:tool')")
+ fun offShelve(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS, data = managementService.offShelve(id))
+
+ /**
+ * Delete tool
+ *
+ * @param id Tool ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除工具")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:delete:tool')")
+ fun delete(@PathVariable id: Long): ResponseResult =
+ if (managementService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/StoreController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/StoreController.kt
new file mode 100644
index 0000000..f3c1e58
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/StoreController.kt
@@ -0,0 +1,119 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.PageSortParam
+import top.fatweb.oxygen.api.param.tool.ToolFavoriteAddParam
+import top.fatweb.oxygen.api.param.tool.ToolFavoriteRemoveParam
+import top.fatweb.oxygen.api.param.tool.ToolStoreGetParam
+import top.fatweb.oxygen.api.service.tool.IStoreService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolVo
+
+/**
+ * Tool store controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IStoreService
+ */
+@BaseController(path = ["/tool/store"], name = "工具商店", description = "工具商店相关接口")
+class StoreController(
+ private val storeService: IStoreService
+) {
+ /**
+ * Get store tool paging information
+ *
+ * @param toolStoreGetParam Get tool parameters in tool store
+ * @return Response object includes store tool paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolStoreGetParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(description = "获取商店工具")
+ @GetMapping
+ fun get(@Valid toolStoreGetParam: ToolStoreGetParam): ResponseResult> =
+ ResponseResult.databaseSuccess(data = storeService.getPage(toolStoreGetParam))
+
+ /**
+ * Get store tool paging information by username
+ *
+ * @param username Username
+ * @param pageSortParam Page sort parameters
+ * @return Response object includes store tool paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see PageSortParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(description = "获取商店指定用户工具")
+ @GetMapping("/{username}")
+ fun get(@PathVariable username: String, @Valid pageSortParam: PageSortParam): ResponseResult> =
+ ResponseResult.databaseSuccess(data = storeService.getPage(pageSortParam, username.trim()))
+
+ /**
+ * Add favorite tool
+ *
+ * @param toolFavoriteAddParam Add favorite tool parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolFavoriteAddParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "收藏工具")
+ @PostMapping("/favorite")
+ fun addFavorite(@RequestBody toolFavoriteAddParam: ToolFavoriteAddParam): ResponseResult {
+ storeService.addFavorite(toolFavoriteAddParam)
+
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS)
+ }
+
+ /**
+ * Remove favorite tool
+ *
+ * @param toolFavoriteRemoveParam Remove favorite tool parameters
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolFavoriteRemoveParam
+ * @see ResponseResult
+ */
+ @Operation(summary = "收藏工具")
+ @DeleteMapping("/favorite")
+ fun addFavorite(@RequestBody toolFavoriteRemoveParam: ToolFavoriteRemoveParam): ResponseResult {
+ storeService.removeFavorite(toolFavoriteRemoveParam)
+
+ return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ }
+
+ /**
+ * Get favorite tool
+ *
+ * @param pageSortParam Page sort parameters
+ * @return Response object includes favorite tool paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see PageSortParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolVo
+ */
+ @Trim
+ @Operation(summary = "获取收藏工具")
+ @GetMapping("/favorite")
+ fun getFavorite(@Valid pageSortParam: PageSortParam): ResponseResult> =
+ ResponseResult.databaseSuccess(data = storeService.getFavorite(pageSortParam))
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/TemplateController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/TemplateController.kt
new file mode 100644
index 0000000..3cbed35
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/tool/TemplateController.kt
@@ -0,0 +1,120 @@
+package top.fatweb.oxygen.api.controller.tool
+
+import io.swagger.v3.oas.annotations.Operation
+import jakarta.validation.Valid
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+import top.fatweb.oxygen.api.annotation.BaseController
+import top.fatweb.oxygen.api.annotation.Trim
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.param.tool.ToolTemplateAddParam
+import top.fatweb.oxygen.api.param.tool.ToolTemplateGetParam
+import top.fatweb.oxygen.api.param.tool.ToolTemplateUpdateParam
+import top.fatweb.oxygen.api.service.tool.IToolTemplateService
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
+
+/**
+ * Tool template management controller
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IToolTemplateService
+ */
+@BaseController(path = ["/system/tool/template"], name = "工具模板管理", description = "工具模板管理相关接口")
+class TemplateController(
+ private val toolTemplateService: IToolTemplateService
+) {
+ /**
+ * Get tool template by ID
+ *
+ * @param id Tool template ID
+ * @return Response object includes tool template information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ * @see ToolTemplateVo
+ */
+ @Operation(summary = "获取单个模板")
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:query:template')")
+ fun getOne(@PathVariable id: Long): ResponseResult =
+ ResponseResult.databaseSuccess(data = toolTemplateService.getOne(id))
+
+ /**
+ * Get tool template paging information
+ *
+ * @param toolTemplateGetParam Get tool template parameters
+ * @return Response object includes tool template paging information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplateGetParam
+ * @see ResponseResult
+ * @see PageVo
+ * @see ToolTemplateVo
+ */
+ @Operation(summary = "获取模板")
+ @GetMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:query:template')")
+ fun get(toolTemplateGetParam: ToolTemplateGetParam?): ResponseResult> =
+ ResponseResult.databaseSuccess(data = toolTemplateService.get(toolTemplateGetParam))
+
+ /**
+ * Add tool template
+ *
+ * @param toolTemplateAddParam Add tool template parameters
+ * @return Response object includes tool template information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplateAddParam
+ * @see ResponseResult
+ * @see ToolTemplateVo
+ */
+ @Trim
+ @Operation(summary = "添加模板")
+ @PostMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:add:template')")
+ fun add(@RequestBody @Valid toolTemplateAddParam: ToolTemplateAddParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_INSERT_SUCCESS,
+ data = toolTemplateService.add(toolTemplateAddParam)
+ )
+
+ /**
+ * Update tool template
+ *
+ * @param toolTemplateUpdateParam Update tool template parameters
+ * @return Response object includes tool template information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplateUpdateParam
+ * @see ResponseResult
+ * @see ToolTemplateVo
+ */
+ @Trim
+ @Operation(summary = "更新模板")
+ @PutMapping
+ @PreAuthorize("hasAnyAuthority('system:tool:modify:template')")
+ fun update(@RequestBody @Valid toolTemplateUpdateParam: ToolTemplateUpdateParam): ResponseResult =
+ ResponseResult.databaseSuccess(
+ ResponseCode.DATABASE_UPDATE_SUCCESS,
+ data = toolTemplateService.update(toolTemplateUpdateParam)
+ )
+
+ /**
+ * Delete tool template
+ *
+ * @param id Tool template ID
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseResult
+ */
+ @Operation(summary = "删除模板")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyAuthority('system:tool:delete:template')")
+ fun delete(@PathVariable id: Long): ResponseResult =
+ if (toolTemplateService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
+ else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/FuncConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/FuncConverter.kt
new file mode 100644
index 0000000..ed5c227
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/FuncConverter.kt
@@ -0,0 +1,29 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.Func
+import top.fatweb.oxygen.api.vo.permission.base.FuncVo
+
+/**
+ * Function converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object FuncConverter {
+ /**
+ * Convert Func object into FuncVo object
+ *
+ * @param func Func object
+ * @return FuncVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Func
+ * @see FuncVo
+ */
+ fun funcToFuncVo(func: Func) = FuncVo(
+ id = func.id,
+ name = func.name,
+ parentId = func.parentId,
+ menuId = func.menuId
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/GroupConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/GroupConverter.kt
new file mode 100644
index 0000000..0f544e9
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/GroupConverter.kt
@@ -0,0 +1,124 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.oxygen.api.entity.permission.Group
+import top.fatweb.oxygen.api.entity.permission.Role
+import top.fatweb.oxygen.api.param.permission.group.GroupAddParam
+import top.fatweb.oxygen.api.param.permission.group.GroupUpdateParam
+import top.fatweb.oxygen.api.param.permission.group.GroupUpdateStatusParam
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.GroupWithRoleVo
+import top.fatweb.oxygen.api.vo.permission.base.GroupVo
+
+/**
+ * Group converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object GroupConverter {
+ /**
+ * Convert Group object into GroupVo object
+ *
+ * @param group Group object
+ * @return GroupVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Group
+ * @see GroupVo
+ */
+ fun groupToGroupVo(group: Group) = GroupVo(
+ id = group.id,
+ name = group.name,
+ enable = group.enable?.let { it == 1},
+ createTime = group.createTime,
+ updateTime = group.updateTime
+ )
+
+ /**
+ * Convert Group object into GroupWithRoleVo object
+ *
+ * @param group Group object
+ * @return GroupWithRoleVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Group
+ * @see GroupWithRoleVo
+ */
+ fun groupToGroupWithRoleVo(group: Group) = GroupWithRoleVo(
+ id = group.id,
+ name = group.name,
+ enable = group.enable?.let { it == 1},
+ createTime = group.createTime,
+ updateTime = group.updateTime,
+ roles = group.roles?.map(RoleConverter::roleToRoleVo)
+ )
+
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @param groupPage IPage object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see Group
+ * @see PageVo
+ * @see GroupWithRoleVo
+ */
+ fun groupPageToGroupWithRolePageVo(groupPage: IPage) = PageVo(
+ total = groupPage.total,
+ pages = groupPage.pages,
+ size = groupPage.size,
+ current = groupPage.current,
+ records = groupPage.records.map(::groupToGroupWithRoleVo)
+ )
+
+ /**
+ * Convert GroupAddParam object into Group object
+ *
+ * @param groupAddParam GroupAddParam object
+ * @return Group object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupAddParam
+ * @see Group
+ */
+ fun groupAddParamToGroup(groupAddParam: GroupAddParam) = Group().apply {
+ name = groupAddParam.name
+ enable = if (groupAddParam.enable) 1 else 0
+ roles = groupAddParam.roleIds?.map { Role().apply { id = it } }
+ }
+
+ /**
+ * Convert GroupUpdateParam object into Group object
+ *
+ * @param groupUpdateParam GroupUpdateParam object
+ * @return Group object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupUpdateParam
+ * @see Group
+ */
+ fun groupUpdateParamToGroup(groupUpdateParam: GroupUpdateParam) = Group().apply {
+ id = groupUpdateParam.id
+ name = groupUpdateParam.name
+ enable = if (groupUpdateParam.enable) 1 else 0
+ roles = groupUpdateParam.roleIds?.map { Role().apply { id = it } }
+ }
+
+ /**
+ * Convert GroupUpdateStatusParam object into Group object
+ *
+ * @param groupUpdateStatusParam GroupUpdateStatusParam object
+ * @return Group object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see GroupUpdateStatusParam
+ * @see Group
+ */
+ fun groupUpdateStatusParamToGroup(groupUpdateStatusParam: GroupUpdateStatusParam) = Group().apply {
+ id = groupUpdateStatusParam.id
+ enable = if (groupUpdateStatusParam.enable) 1 else 0
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/MenuConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/MenuConverter.kt
new file mode 100644
index 0000000..ef573d1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/MenuConverter.kt
@@ -0,0 +1,30 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.Menu
+import top.fatweb.oxygen.api.vo.permission.base.MenuVo
+
+/**
+ * Menu converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object MenuConverter {
+ /**
+ * Convert Menu object into MenuVo object
+ *
+ * @param menu Menu object
+ * @return MenuVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Menu
+ * @see MenuVo
+ */
+ fun menuToMenuVo(menu: Menu) = MenuVo(
+ id = menu.id,
+ name = menu.name,
+ url = menu.url,
+ parentId = menu.parentId,
+ moduleId = menu.moduleId
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/ModuleConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/ModuleConverter.kt
new file mode 100644
index 0000000..4760c51
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/ModuleConverter.kt
@@ -0,0 +1,27 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.Module
+import top.fatweb.oxygen.api.vo.permission.base.ModuleVo
+
+/**
+ * Module converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ModuleConverter {
+ /**
+ * Convert Module object into ModuleVo object
+ *
+ * @param module Module object
+ * @return ModuleVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Module
+ * @see ModuleVo
+ */
+ fun moduleToModuleVo(module: Module) = ModuleVo(
+ id = module.id,
+ name = module.name
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/OperationConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/OperationConverter.kt
new file mode 100644
index 0000000..dd9f0b3
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/OperationConverter.kt
@@ -0,0 +1,29 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.Operation
+import top.fatweb.oxygen.api.vo.permission.base.OperationVo
+
+/**
+ * Operation converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object OperationConverter {
+ /**
+ * Convert Operation object into OperationVo object
+ *
+ * @param operation Operation object
+ * @return OperationVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Operation
+ * @see OperationVo
+ */
+ fun operationToOperationVo(operation: Operation) = OperationVo(
+ id = operation.id,
+ name = operation.name,
+ code = operation.code,
+ funcId = operation.funcId
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/PowerConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/PowerConverter.kt
new file mode 100644
index 0000000..5fea079
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/PowerConverter.kt
@@ -0,0 +1,29 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.PowerSet
+import top.fatweb.oxygen.api.vo.permission.PowerSetVo
+
+/**
+ * Power converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object PowerConverter {
+ /**
+ * Convert PowerSet object into PowerSetVo object
+ *
+ * @param powerSet PowerSet object
+ * @return PowerSetVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see PowerSet
+ * @see PowerSetVo
+ */
+ fun powerSetToPowerSetVo(powerSet: PowerSet) = PowerSetVo(
+ moduleList = powerSet.moduleList?.map(ModuleConverter::moduleToModuleVo),
+ menuList = powerSet.menuList?.map(MenuConverter::menuToMenuVo),
+ funcList = powerSet.funcList?.map(FuncConverter::funcToFuncVo),
+ operationList = powerSet.operationList?.map(OperationConverter::operationToOperationVo)
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/RoleConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/RoleConverter.kt
new file mode 100644
index 0000000..f6bd0b7
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/RoleConverter.kt
@@ -0,0 +1,127 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.oxygen.api.entity.permission.Power
+import top.fatweb.oxygen.api.entity.permission.Role
+import top.fatweb.oxygen.api.param.permission.role.RoleAddParam
+import top.fatweb.oxygen.api.param.permission.role.RoleUpdateParam
+import top.fatweb.oxygen.api.param.permission.role.RoleUpdateStatusParam
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.RoleWithPowerVo
+import top.fatweb.oxygen.api.vo.permission.base.RoleVo
+
+/**
+ * Role converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object RoleConverter {
+ /**
+ * Convert Role object into RoleVo object
+ *
+ * @param role Role object
+ * @return RoleVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ * @see RoleVo
+ */
+ fun roleToRoleVo(role: Role) = RoleVo(
+ id = role.id,
+ name = role.name,
+ enable = role.enable?.let { it == 1},
+ createTime = role.createTime,
+ updateTime = role.updateTime
+ )
+
+ /**
+ * Convert Role object into RoleWithPowerVo object
+ *
+ * @param role Role object
+ * @return RoleWithPowerVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ * @see RoleWithPowerVo
+ */
+ fun roleToRoleWithPowerVo(role: Role) = RoleWithPowerVo(
+ id = role.id,
+ name = role.name,
+ enable = role.enable?.let { it == 1},
+ createTime = role.createTime,
+ updateTime = role.updateTime,
+ modules = role.modules?.map(ModuleConverter::moduleToModuleVo),
+ menus = role.menus?.map(MenuConverter::menuToMenuVo),
+ funcs = role.funcs?.map(FuncConverter::funcToFuncVo),
+ operations = role.operations?.map(OperationConverter::operationToOperationVo)
+ )
+
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @param rolePage IPage object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see Role
+ * @see PageVo
+ * @see RoleWithPowerVo
+ */
+ fun rolePageToRoleWithPowerPageVo(rolePage: IPage) = PageVo(
+ total = rolePage.total,
+ pages = rolePage.pages,
+ size = rolePage.size,
+ current = rolePage.current,
+ records = rolePage.records.map(::roleToRoleWithPowerVo)
+ )
+
+ /**
+ * Convert RoleAddParam object into Role object
+ *
+ * @param roleAddParam RoleAddParam object
+ * @return Role object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleAddParam
+ * @see Role
+ */
+ fun roleAddParamToRole(roleAddParam: RoleAddParam) = Role().apply {
+ name = roleAddParam.name
+ enable = if (roleAddParam.enable) 1 else 0
+ powers = roleAddParam.powerIds?.map { Power().apply { id = it } }
+ }
+
+ /**
+ * Convert RoleUpdateParam into Role object
+ *
+ * @param roleUpdateParam RoleUpdateParam object
+ * @return Role object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleUpdateParam
+ * @see Role
+ */
+ fun roleUpdateParamToRole(roleUpdateParam: RoleUpdateParam) = Role().apply {
+ id = roleUpdateParam.id
+ name = roleUpdateParam.name
+ enable = if (roleUpdateParam.enable) 1 else 0
+ powers = roleUpdateParam.powerIds?.map { Power().apply { id = it } }
+ }
+
+ /**
+ * Convert RoleUpdateStatusParam object into Role object
+ *
+ * @param roleUpdateStatusParam RoleUpdateStatusParam object
+ * @return Role object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RoleUpdateStatusParam
+ * @see Role
+ */
+ fun roleUpdateStatusParamToRole(roleUpdateStatusParam: RoleUpdateStatusParam) = Role().apply {
+ id = roleUpdateStatusParam.id
+ enable = if (roleUpdateStatusParam.enable) 1 else 0
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt
new file mode 100644
index 0000000..dec373e
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt
@@ -0,0 +1,217 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.avatargenerator.GitHubAvatar
+import top.fatweb.oxygen.api.entity.permission.Group
+import top.fatweb.oxygen.api.entity.permission.Role
+import top.fatweb.oxygen.api.entity.permission.User
+import top.fatweb.oxygen.api.entity.permission.UserInfo
+import top.fatweb.oxygen.api.param.permission.user.UserAddParam
+import top.fatweb.oxygen.api.param.permission.user.UserUpdateParam
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.permission.UserWithInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithPasswordRoleInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithPowerInfoVo
+import top.fatweb.oxygen.api.vo.permission.UserWithRoleInfoVo
+
+/**
+ * User converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object UserConverter {
+ /**
+ * Convert User object into UserWithPowerInfoVo object
+ *
+ * @param user User object
+ * @return UserWithPowerInfoVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ * @see UserWithPowerInfoVo
+ */
+ fun userToUserWithPowerInfoVo(user: User) = UserWithPowerInfoVo(
+ id = user.id,
+ username = user.username,
+ twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
+ verified = user.verify.isNullOrBlank(),
+ locking = user.locking?.let { it == 1 },
+ expiration = user.expiration,
+ credentialsExpiration = user.credentialsExpiration,
+ enable = user.enable?.let { it == 1 },
+ currentLoginTime = user.currentLoginTime,
+ currentLoginIp = user.currentLoginIp,
+ lastLoginTime = user.lastLoginTime,
+ lastLoginIp = user.lastLoginIp,
+ createTime = user.createTime,
+ updateTime = user.updateTime,
+ userInfo = user.userInfo?.let(UserInfoConverter::userInfoToUserInfoVo),
+ modules = user.modules?.map(ModuleConverter::moduleToModuleVo),
+ menus = user.menus?.map(MenuConverter::menuToMenuVo),
+ funcs = user.funcs?.map(FuncConverter::funcToFuncVo),
+ operations = user.operations?.map(OperationConverter::operationToOperationVo)
+ )
+
+ /**
+ * Convert User object into UserWithRoleInfoVo object
+ *
+ * @param user User object
+ * @return UserWithRoleInfoVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ * @see UserWithRoleInfoVo
+ */
+ fun userToUserWithRoleInfoVo(user: User) = UserWithRoleInfoVo(
+ id = user.id,
+ username = user.username,
+ twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
+ verify = user.verify,
+ locking = user.locking?.let { it == 1 },
+ expiration = user.expiration,
+ credentialsExpiration = user.credentialsExpiration,
+ enable = user.enable?.let { it == 1 },
+ currentLoginTime = user.currentLoginTime,
+ currentLoginIp = user.currentLoginIp,
+ lastLoginTime = user.lastLoginTime,
+ lastLoginIp = user.lastLoginIp,
+ createTime = user.createTime,
+ updateTime = user.updateTime,
+ userInfo = user.userInfo?.let(UserInfoConverter::userInfoToUserInfoVo),
+ roles = user.roles?.map(RoleConverter::roleToRoleVo),
+ groups = user.groups?.map(GroupConverter::groupToGroupVo)
+ )
+
+ /**
+ * Convert User object into UserWithInfoVo object
+ *
+ * @param user User object
+ * @return UserWithInfoVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ * @see UserWithInfoVo
+ */
+ fun userToUserWithInfoVo(user: User) = UserWithInfoVo(
+ id = user.id,
+ username = user.username,
+ twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
+ verified = user.verify.isNullOrBlank(),
+ locking = user.locking?.let { it == 1 },
+ expiration = user.expiration,
+ credentialsExpiration = user.credentialsExpiration,
+ enable = user.enable?.let { it == 1 },
+ currentLoginTime = user.currentLoginTime,
+ currentLoginIp = user.currentLoginIp,
+ lastLoginTime = user.lastLoginTime,
+ lastLoginIp = user.lastLoginIp,
+ createTime = user.createTime,
+ updateTime = user.updateTime,
+ userInfo = user.userInfo?.let(UserInfoConverter::userInfoToUserInfoVo)
+ )
+
+ /**
+ * Convert User object into UserWithPasswordRoleInfoVo object
+ *
+ * @param user User object
+ * @return UserWithPasswordRoleInfoVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ * @see UserWithPasswordRoleInfoVo
+ */
+ fun userToUserWithPasswordRoleInfoVo(user: User) = UserWithPasswordRoleInfoVo(
+ id = user.id,
+ username = user.username,
+ password = user.password,
+ twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
+ verify = user.verify,
+ locking = user.locking?.let { it == 1 },
+ expiration = user.expiration,
+ credentialsExpiration = user.credentialsExpiration,
+ enable = user.enable?.let { it == 1 },
+ currentLoginTime = user.currentLoginTime,
+ currentLoginIp = user.currentLoginIp,
+ lastLoginTime = user.lastLoginTime,
+ lastLoginIp = user.lastLoginIp,
+ createTime = user.createTime,
+ updateTime = user.updateTime,
+ userInfo = user.userInfo?.let(UserInfoConverter::userInfoToUserInfoVo),
+ roles = user.roles?.map(RoleConverter::roleToRoleVo),
+ groups = user.groups?.map(GroupConverter::groupToGroupVo)
+ )
+
+ /**
+ * Convert UserAddParam object into User object
+ *
+ * @param userAddParam UserAddParam object
+ * @return User object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserAddParam
+ * @see User
+ */
+ fun userAddParamToUser(userAddParam: UserAddParam) = User().apply {
+ username = userAddParam.username
+ password = userAddParam.password
+ locking = if (userAddParam.locking) 1 else 0
+ expiration = userAddParam.expiration
+ credentialsExpiration = userAddParam.credentialsExpiration
+ enable = if (userAddParam.enable) 1 else 0
+ userInfo = UserInfo().apply {
+ nickname = userAddParam.nickname ?: userAddParam.username
+ avatar = userAddParam.avatar ?: GitHubAvatar.newAvatarBuilder().build()
+ .createAsBase64((Long.MIN_VALUE..Long.MAX_VALUE).random())
+ email = userAddParam.email
+ }
+ roles = userAddParam.roleIds?.map { Role().apply { id = it } }
+ groups = userAddParam.groupIds?.map { Group().apply { id = it } }
+ }
+
+ /**
+ * Convert UserUpdateParam object into User object
+ *
+ * @param userUpdateParam UserUpdateParam object
+ * @return User object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserUpdateParam
+ * @see User
+ */
+ fun userUpdateParamToUser(userUpdateParam: UserUpdateParam) = User().apply {
+ id = userUpdateParam.id
+ username = userUpdateParam.username
+ locking = if (userUpdateParam.locking && userUpdateParam.id != 0L) 1 else 0
+ expiration = if (userUpdateParam.id != 0L) userUpdateParam.expiration else null
+ credentialsExpiration = userUpdateParam.credentialsExpiration
+ enable = if (userUpdateParam.enable || userUpdateParam.id == 0L) 1 else 0
+ userInfo = UserInfo().apply {
+ nickname = userUpdateParam.nickname
+ avatar = userUpdateParam.avatar
+ email = userUpdateParam.email
+ }
+ roles = if (userUpdateParam.id != 0L) userUpdateParam.roleIds?.map { Role().apply { id = it } } else null
+ groups = if (userUpdateParam.id != 0L) userUpdateParam.groupIds?.map { Group().apply { id = it } } else null
+ }
+
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @param userPage IPage object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see User
+ * @see PageVo
+ * @see UserWithRoleInfoVo
+ */
+ fun userPageToUserWithRoleInfoPageVo(userPage: IPage) = PageVo(
+ total = userPage.total,
+ pages = userPage.pages,
+ size = userPage.size,
+ current = userPage.current,
+ records = userPage.records.map(::userToUserWithRoleInfoVo)
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserInfoConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserInfoConverter.kt
new file mode 100644
index 0000000..89bf651
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserInfoConverter.kt
@@ -0,0 +1,32 @@
+package top.fatweb.oxygen.api.converter.permission
+
+import top.fatweb.oxygen.api.entity.permission.UserInfo
+import top.fatweb.oxygen.api.vo.permission.base.UserInfoVo
+
+/**
+ * User information converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object UserInfoConverter {
+ /**
+ * Convert UserInfo object into UserInfoVo object
+ *
+ * @param userInfo UserInfo object
+ * @return UserInfoVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserInfo
+ * @see UserInfoVo
+ */
+ fun userInfoToUserInfoVo(userInfo: UserInfo) = UserInfoVo(
+ id = userInfo.id,
+ userId = userInfo.userId,
+ nickname = userInfo.nickname,
+ avatar = userInfo.avatar,
+ email = userInfo.email,
+ createTime = userInfo.createTime,
+ updateTime = userInfo.updateTime
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SettingsConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SettingsConverter.kt
new file mode 100644
index 0000000..8f2d554
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SettingsConverter.kt
@@ -0,0 +1,46 @@
+package top.fatweb.oxygen.api.converter.system
+
+import top.fatweb.oxygen.api.entity.system.SensitiveWord
+import top.fatweb.oxygen.api.param.system.SensitiveWordAddParam
+import top.fatweb.oxygen.api.vo.system.SensitiveWordVo
+
+/**
+ * Settings converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object SettingsConverter {
+ /**
+ * Convert SensitiveWord object into SensitiveWordVo object
+ *
+ * @param sensitiveWord SensitiveWord object
+ * @return SensitiveWordVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SensitiveWord
+ * @see SensitiveWordVo
+ */
+ fun sensitiveWordToSensitiveWordVo(sensitiveWord: SensitiveWord) = SensitiveWordVo(
+ id = sensitiveWord.id,
+ word = sensitiveWord.word,
+ useFor = sensitiveWord.useFor?.map(SensitiveWord.Use::valueOf)?.toSet(),
+ enable = sensitiveWord.enable?.let { it == 1}
+ )
+
+ /**
+ * Convert SensitiveWordAddParam object into SensitiveWord object
+ *
+ * @param sensitiveWordAddParam SensitiveWordAddParam object
+ * @return SensitiveWord object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SensitiveWordAddParam
+ * @see SensitiveWord
+ */
+ fun sensitiveWordAddParamToSensitiveWord(sensitiveWordAddParam: SensitiveWordAddParam) = SensitiveWord().apply {
+ word = sensitiveWordAddParam.word
+ useFor = sensitiveWordAddParam.useFor.map(SensitiveWord.Use::code).toSet()
+ enable = if (sensitiveWordAddParam.enable) 1 else 0
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SysLogConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SysLogConverter.kt
new file mode 100644
index 0000000..ce7f05c
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/system/SysLogConverter.kt
@@ -0,0 +1,53 @@
+package top.fatweb.oxygen.api.converter.system
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.oxygen.api.entity.system.SysLog
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.system.SysLogVo
+
+/**
+ * System log converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object SysLogConverter {
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @param syslogPage IPage object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see SysLog
+ * @see PageVo
+ * @see SysLogVo
+ */
+ fun sysLogPageToSysLogPageVo(syslogPage: IPage) = PageVo(
+ syslogPage.total,
+ syslogPage.pages,
+ syslogPage.size,
+ syslogPage.current,
+ syslogPage.records.map { sysLog ->
+ SysLogVo(
+ id = sysLog.id,
+ logType = sysLog.logType,
+ operateUserId = sysLog.operateUserId,
+ operateTime = sysLog.operateTime,
+ requestUri = sysLog.requestUri,
+ requestMethod = sysLog.requestMethod,
+ requestParams = sysLog.requestParams,
+ requestIp = sysLog.requestIp,
+ requestServerAddress = sysLog.requestServerAddress,
+ exception = sysLog.exception?.let { it == 1},
+ exceptionInfo = sysLog.exceptionInfo,
+ startTime = sysLog.startTime,
+ endTime = sysLog.endTime,
+ executeTime = sysLog.executeTime,
+ userAgent = sysLog.userAgent,
+ operateUsername = sysLog.operateUsername
+ )
+ })
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolBaseConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolBaseConverter.kt
new file mode 100644
index 0000000..fb76be6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolBaseConverter.kt
@@ -0,0 +1,77 @@
+package top.fatweb.oxygen.api.converter.tool
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.oxygen.api.entity.tool.ToolBase
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolBaseVo
+import top.fatweb.oxygen.api.vo.tool.ToolDataVo
+
+/**
+ * Tool base converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ToolBaseConverter {
+ /**
+ * Convert ToolBase object into ToolBaseVo object
+ *
+ * @param toolBase ToolBase object
+ * @return ToolBaseVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBase
+ * @see ToolBaseVo
+ */
+ fun toolBaseToToolBaseVo(toolBase: ToolBase) = ToolBaseVo(
+ id = toolBase.id,
+ name = toolBase.name,
+ source = toolBase.source?.let(ToolDataConverter::toolDataToToolDataVo),
+ dist = toolBase.dist?.let(ToolDataConverter::toolDataToToolDataVo),
+ platform = toolBase.platform,
+ compiled = toolBase.compiled?.let { it == 1},
+ createTime = toolBase.createTime,
+ updateTime = toolBase.updateTime
+ )
+
+ /**
+ * Convert ToolBase object into ToolBaseVo object by get page
+ *
+ * @param toolBase ToolBase object
+ * @return ToolBaseVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBase
+ * @see ToolBaseVo
+ */
+ fun toolBaseToToolBaseVoByGetList(toolBase: ToolBase) = ToolBaseVo(
+ id = toolBase.id,
+ name = toolBase.name,
+ source = ToolDataVo(id = toolBase.sourceId, data = null, createTime = null, updateTime = null),
+ dist = ToolDataVo(id = toolBase.distId, data = null, createTime = null, updateTime = null),
+ platform = toolBase.platform,
+ compiled = toolBase.compiled?.let { it == 1},
+ createTime = toolBase.createTime,
+ updateTime = toolBase.updateTime
+ )
+
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @param toolBasePage IPage object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see ToolBase
+ * @see PageVo
+ * @see ToolBaseVo
+ */
+ fun toolBasePageToToolBasePageVo(toolBasePage: IPage) = PageVo(
+ total = toolBasePage.total,
+ pages = toolBasePage.pages,
+ size = toolBasePage.size,
+ current = toolBasePage.current,
+ records = toolBasePage.records.map(::toolBaseToToolBaseVoByGetList)
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolCategoryConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolCategoryConverter.kt
new file mode 100644
index 0000000..9efcb6f
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolCategoryConverter.kt
@@ -0,0 +1,63 @@
+package top.fatweb.oxygen.api.converter.tool
+
+import top.fatweb.oxygen.api.entity.tool.ToolCategory
+import top.fatweb.oxygen.api.param.tool.ToolCategoryAddParam
+import top.fatweb.oxygen.api.param.tool.ToolCategoryUpdateParam
+import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
+
+/**
+ * Tool category converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ToolCategoryConverter {
+ /**
+ * Convert ToolCategory object into ToolCategoryVo object
+ *
+ * @param toolCategory ToolCategory object
+ * @return ToolCategoryVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCategory
+ * @see ToolCategoryVo
+ */
+ fun toolCategoryToToolCategoryVo(toolCategory: ToolCategory) = ToolCategoryVo(
+ id = toolCategory.id,
+ name = toolCategory.name,
+ enable = toolCategory.enable?.let { it == 1},
+ createTime = toolCategory.createTime,
+ updateTime = toolCategory.updateTime
+ )
+
+ /**
+ * Convert ToolCategoryAddParam object into ToolCategory object
+ *
+ * @param toolCategoryAddParam ToolCategoryAddParam object
+ * @return ToolCateGory object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCategoryAddParam
+ * @see ToolCategory
+ */
+ fun toolCategoryAddParamToToolCategory(toolCategoryAddParam: ToolCategoryAddParam) = ToolCategory().apply {
+ name = toolCategoryAddParam.name
+ enable = if (toolCategoryAddParam.enable) 1 else 0
+ }
+
+ /**
+ * Convert ToolCategoryUpdateParam object into ToolCategory object
+ *
+ * @param toolCategoryUpdateParam ToolCategoryUpdateParam object
+ * @return ToolCategory object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolCategoryUpdateParam
+ * @see ToolCategory
+ */
+ fun toolCategoryUpdateParamToToolCategory(toolCategoryUpdateParam: ToolCategoryUpdateParam) = ToolCategory().apply {
+ id = toolCategoryUpdateParam.id
+ name = toolCategoryUpdateParam.name
+ enable = toolCategoryUpdateParam.enable?. let { if (it) 1 else 0 }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolConverter.kt
new file mode 100644
index 0000000..249b69d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolConverter.kt
@@ -0,0 +1,67 @@
+package top.fatweb.oxygen.api.converter.tool
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page
+import top.fatweb.oxygen.api.converter.permission.UserConverter
+import top.fatweb.oxygen.api.entity.tool.Tool
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolVo
+
+/**
+ * Tool converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ToolConverter {
+ /**
+ * Convert Tool object into ToolVo object
+ *
+ * @param tool Tool object
+ * @return ToolVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ * @see ToolVo
+ */
+ fun toolToToolVo(tool: Tool) = ToolVo(
+ id = tool.id,
+ name = tool.name,
+ toolId = tool.toolId,
+ icon = tool.icon,
+ platform = tool.platform,
+ description = tool.description,
+ base = tool.base?.let(ToolBaseConverter::toolBaseToToolBaseVo),
+ author = tool.author?.let(UserConverter::userToUserWithInfoVo),
+ ver = tool.ver,
+ keywords = tool.keywords,
+ categories = tool.categories?.map(ToolCategoryConverter::toolCategoryToToolCategoryVo),
+ source = tool.source?.let(ToolDataConverter::toolDataToToolDataVo),
+ dist = tool.dist?.let(ToolDataConverter::toolDataToToolDataVo),
+ entryPoint = tool.entryPoint,
+ publish = tool.publish,
+ review = tool.review,
+ createTime = tool.createTime,
+ updateTime = tool.updateTime,
+ favorite = tool.favorite?.let { it == 1}
+ )
+
+ /**
+ * Convert Page object into PageVo object
+ *
+ * @param toolPage Page object
+ * @return PageVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Page
+ * @see Tool
+ * @see PageVo
+ * @see ToolVo
+ */
+ fun toolPageToToolPageVo(toolPage: Page): PageVo = PageVo(
+ total = toolPage.total,
+ pages = toolPage.pages,
+ size = toolPage.size,
+ current = toolPage.current,
+ records = toolPage.records.map(::toolToToolVo)
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolDataConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolDataConverter.kt
new file mode 100644
index 0000000..117752d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolDataConverter.kt
@@ -0,0 +1,29 @@
+package top.fatweb.oxygen.api.converter.tool
+
+import top.fatweb.oxygen.api.entity.tool.ToolData
+import top.fatweb.oxygen.api.vo.tool.ToolDataVo
+
+/**
+ * Tool data converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ToolDataConverter {
+ /**
+ * Convert ToolData object into ToolDataVo object
+ *
+ * @param toolData ToolData object
+ * @return ToolDataVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolData
+ * @see ToolDataVo
+ */
+ fun toolDataToToolDataVo(toolData: ToolData) = ToolDataVo(
+ id = toolData.id,
+ data = toolData.data,
+ createTime = toolData.createTime,
+ updateTime = toolData.updateTime
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolTemplateConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolTemplateConverter.kt
new file mode 100644
index 0000000..7fb5663
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/tool/ToolTemplateConverter.kt
@@ -0,0 +1,106 @@
+package top.fatweb.oxygen.api.converter.tool
+
+import com.baomidou.mybatisplus.core.metadata.IPage
+import top.fatweb.oxygen.api.entity.tool.ToolTemplate
+import top.fatweb.oxygen.api.vo.PageVo
+import top.fatweb.oxygen.api.vo.tool.ToolBaseVo
+import top.fatweb.oxygen.api.vo.tool.ToolDataVo
+import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
+
+/**
+ * Tool template converter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+object ToolTemplateConverter {
+ /**
+ * Convert ToolTemplate object into ToolTemplateVo object
+ *
+ * @param toolTemplate ToolTemplate object
+ * @return ToolTemplateVo object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplate
+ * @see ToolTemplateVo
+ */
+ fun toolTemplateToToolTemplateVo(toolTemplate: ToolTemplate) = ToolTemplateVo(
+ id = toolTemplate.id,
+ name = toolTemplate.name,
+ base = toolTemplate.base?.let(ToolBaseConverter::toolBaseToToolBaseVo),
+ source = toolTemplate.source?.let(ToolDataConverter::toolDataToToolDataVo),
+ platform = toolTemplate.platform,
+ entryPoint = toolTemplate.entryPoint,
+ enable = toolTemplate.enable?.let { it == 1},
+ createTime = toolTemplate.createTime,
+ updateTime = toolTemplate.updateTime
+ )
+
+ /**
+ * Convert IPage object into PageVo object
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun toolTemplatePageToToolTemplatePageVo(toolTemplatePage: IPage) = PageVo(
+ total = toolTemplatePage.total,
+ pages = toolTemplatePage.pages,
+ size = toolTemplatePage.size,
+ current = toolTemplatePage.current,
+ records = toolTemplatePage.records.map(::toolTemplateToToolTemplateVo)
+ )
+
+ /**
+ * Convert ToolTemplate object into ToolTemplateVo object by list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun toolTemplateToToolTemplateVoByList(toolTemplate: ToolTemplate) = ToolTemplateVo(
+ id = toolTemplate.id,
+ name = toolTemplate.name,
+ base = ToolBaseVo(
+ id = toolTemplate.baseId,
+ name = null,
+ source = null,
+ dist = null,
+ platform = toolTemplate.base?.platform,
+ compiled = null,
+ createTime = null,
+ updateTime = null
+ ),
+ source = ToolDataVo(id = toolTemplate.sourceId, data = null, createTime = null, updateTime = null),
+ platform = toolTemplate.platform,
+ entryPoint = toolTemplate.entryPoint,
+ enable = toolTemplate.enable?.let { it == 1},
+ createTime = toolTemplate.createTime,
+ updateTime = toolTemplate.updateTime
+ )
+
+ /**
+ * Convert ToolTemplate object into ToolTemplateVo object with base dist
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun toolTemplateToToolTemplateVoWithBaseDist(toolTemplate: ToolTemplate) = ToolTemplateVo(
+ id = toolTemplate.id,
+ name = toolTemplate.name,
+ base = ToolBaseVo(
+ id = toolTemplate.baseId,
+ name = toolTemplate.base?.name,
+ source = null,
+ dist = ToolDataVo(id = null, data = toolTemplate.base?.distData, createTime = null, updateTime = null),
+ platform = toolTemplate.base?.platform,
+ compiled = null,
+ createTime = null,
+ updateTime = null
+ ),
+ source = toolTemplate.source?.let(ToolDataConverter::toolDataToToolDataVo),
+ platform = toolTemplate.platform,
+ entryPoint = toolTemplate.entryPoint,
+ enable = toolTemplate.enable?.let { it == 1},
+ createTime = toolTemplate.createTime,
+ updateTime = toolTemplate.updateTime
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/cron/StatisticsCron.kt b/src/main/kotlin/top/fatweb/oxygen/api/cron/StatisticsCron.kt
new file mode 100644
index 0000000..e4b014f
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/cron/StatisticsCron.kt
@@ -0,0 +1,35 @@
+package top.fatweb.oxygen.api.cron
+
+import org.springframework.scheduling.annotation.Scheduled
+import org.springframework.stereotype.Component
+import top.fatweb.oxygen.api.entity.system.StatisticsLog
+import top.fatweb.oxygen.api.properties.SecurityProperties
+import top.fatweb.oxygen.api.service.system.IStatisticsLogService
+import top.fatweb.oxygen.api.util.RedisUtil
+
+/**
+ * Statistics scheduled tasks
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Component
+class StatisticsCron(
+ private val redisUtil: RedisUtil,
+ private val statisticsLogService: IStatisticsLogService
+) {
+ /**
+ * Auto record number of online users
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Scheduled(cron = "0 * * * * *")
+ fun onlineUserCount() {
+ statisticsLogService.save(StatisticsLog().apply {
+ key = StatisticsLog.KeyItem.ONLINE_USERS_COUNT
+ value = redisUtil.keys("${SecurityProperties.jwtIssuer}_login_*")
+ .distinctBy { Regex("${SecurityProperties.jwtIssuer}_login_(.*):.*").matchEntire(it)?.groupValues?.getOrNull(1) }.size.toString()
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/common/BusinessCode.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/BusinessCode.kt
new file mode 100644
index 0000000..5b1f363
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/BusinessCode.kt
@@ -0,0 +1,49 @@
+package top.fatweb.oxygen.api.entity.common
+
+/**
+ * Business code entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+enum class BusinessCode(val code: Int) {
+ /**
+ * System
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ SYSTEM(100),
+
+ /**
+ * Permission
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ PERMISSION(200),
+
+ /**
+ * Database
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ DATABASE(300),
+
+ /**
+ * Tool
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ TOOL(400),
+
+ /**
+ * Avatar API
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ API_AVATAR(501)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt
new file mode 100644
index 0000000..c95a4a5
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt
@@ -0,0 +1,81 @@
+package top.fatweb.oxygen.api.entity.common
+
+/**
+ * Response code entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+enum class ResponseCode(val code: Int) {
+ SYSTEM_OK(BusinessCode.SYSTEM, 0),
+
+ SYSTEM_ERROR(BusinessCode.SYSTEM, 50),
+ SYSTEM_TIMEOUT(BusinessCode.SYSTEM, 51),
+ SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 52),
+ SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 53),
+ SYSTEM_INVALID_CAPTCHA_CODE(BusinessCode.SYSTEM, 54),
+ SYSTEM_REQUEST_TOO_FREQUENT(BusinessCode.SYSTEM, 55),
+ SYSTEM_MATCH_SENSITIVE_WORD(BusinessCode.SYSTEM, 56),
+
+ PERMISSION_LOGIN_SUCCESS(BusinessCode.PERMISSION, 0),
+ PERMISSION_PASSWORD_CHANGE_SUCCESS(BusinessCode.PERMISSION, 1),
+ PERMISSION_LOGOUT_SUCCESS(BusinessCode.PERMISSION, 2),
+ PERMISSION_TOKEN_RENEW_SUCCESS(BusinessCode.PERMISSION, 3),
+ PERMISSION_REGISTER_SUCCESS(BusinessCode.PERMISSION, 4),
+ PERMISSION_RESEND_SUCCESS(BusinessCode.PERMISSION, 5),
+ PERMISSION_VERIFY_SUCCESS(BusinessCode.PERMISSION, 6),
+ PERMISSION_FORGET_SUCCESS(BusinessCode.PERMISSION, 7),
+ PERMISSION_RETRIEVE_SUCCESS(BusinessCode.PERMISSION, 8),
+
+ PERMISSION_UNAUTHORIZED(BusinessCode.PERMISSION, 50),
+ PERMISSION_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51),
+ PERMISSION_ACCESS_DENIED(BusinessCode.PERMISSION, 52),
+ PERMISSION_USER_LOCKED(BusinessCode.PERMISSION, 53),
+ PERMISSION_USER_EXPIRED(BusinessCode.PERMISSION, 54),
+ PERMISSION_USER_CREDENTIALS_EXPIRED(BusinessCode.PERMISSION, 55),
+ PERMISSION_USER_DISABLE(BusinessCode.PERMISSION, 56),
+ PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR(BusinessCode.PERMISSION, 57),
+ PERMISSION_OLD_PASSWORD_NOT_MATCH(BusinessCode.PERMISSION, 58),
+ PERMISSION_LOGOUT_FAILED(BusinessCode.PERMISSION, 59),
+ PERMISSION_TOKEN_ILLEGAL(BusinessCode.PERMISSION, 60),
+ PERMISSION_TOKEN_HAS_EXPIRED(BusinessCode.PERMISSION, 61),
+ PERMISSION_NO_VERIFICATION_REQUIRED(BusinessCode.PERMISSION, 62),
+ PERMISSION_VERIFY_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 63),
+ PERMISSION_ACCOUNT_NEED_INIT(BusinessCode.PERMISSION, 64),
+ PERMISSION_USER_NOT_FOUND(BusinessCode.PERMISSION, 65),
+ PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 66),
+ PERMISSION_ACCOUNT_NEED_RESET_PASSWORD(BusinessCode.PERMISSION, 67),
+ PERMISSION_NEED_TWO_FACTOR(BusinessCode.PERMISSION, 68),
+ PERMISSION_ALREADY_HAS_TWO_FACTOR(BusinessCode.PERMISSION, 69),
+ PERMISSION_NO_TWO_FACTOR_FOUND(BusinessCode.PERMISSION, 70),
+ PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR(BusinessCode.PERMISSION, 71),
+
+ DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0),
+ DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5),
+ DATABASE_INSERT_SUCCESS(BusinessCode.DATABASE, 10),
+ DATABASE_INSERT_FAILED(BusinessCode.DATABASE, 15),
+ DATABASE_UPDATE_SUCCESS(BusinessCode.DATABASE, 20),
+ DATABASE_UPDATE_FAILED(BusinessCode.DATABASE, 25),
+ DATABASE_DELETE_SUCCESS(BusinessCode.DATABASE, 30),
+ DATABASE_DELETE_FAILED(BusinessCode.DATABASE, 35),
+ DATABASE_EXECUTE_ERROR(BusinessCode.DATABASE, 50),
+ DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 51),
+ DATABASE_NO_RECORD_FOUND(BusinessCode.DATABASE, 52),
+ DATABASE_RECORD_ALREADY_EXISTS(BusinessCode.DATABASE, 53),
+
+ TOOL_SUBMIT_SUCCESS(BusinessCode.TOOL, 10),
+ TOOL_CANCEL_SUCCESS(BusinessCode.TOOL, 11),
+ TOOL_ILLEGAL_VERSION(BusinessCode.TOOL, 50),
+ TOOL_UNDER_REVIEW(BusinessCode.TOOL, 51),
+ TOOL_NOT_UNDER_REVIEW(BusinessCode.TOOL, 52),
+ TOOL_HAS_UNPUBLISHED_VERSION(BusinessCode.TOOL, 53),
+ TOOL_HAS_NOT_BEEN_PUBLISHED(BusinessCode.TOOL, 54),
+ TOOL_HAS_BEEN_PUBLISHED(BusinessCode.TOOL, 55),
+ TOOL_SUBMIT_ERROR(BusinessCode.TOOL, 60),
+ TOOL_CANCEL_ERROR(BusinessCode.TOOL, 61),
+
+ API_AVATAR_SUCCESS(BusinessCode.API_AVATAR, 0),
+ API_AVATAR_ERROR(BusinessCode.API_AVATAR, 50);
+
+ constructor(businessCode: BusinessCode, code: Int) : this(businessCode.code * 100 + code)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseResult.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseResult.kt
new file mode 100644
index 0000000..7c866fe
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseResult.kt
@@ -0,0 +1,103 @@
+package top.fatweb.oxygen.api.entity.common
+
+import io.swagger.v3.oas.annotations.media.Schema
+import java.io.Serializable
+
+/**
+ * Response result entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+class ResponseResult private constructor(
+ @Schema(description = "响应码", defaultValue = "200") val code: Int,
+
+ @Schema(description = "是否调用成功") val success: Boolean,
+
+ @Schema(description = "信息") val msg: String,
+
+ @Schema(description = "数据") val data: T?
+) : Serializable {
+ companion object {
+ /**
+ * Build response result object
+ *
+ * @param code Response code
+ * @param success Is successful
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun build(code: Int, success: Boolean, msg: String, data: T?) =
+ ResponseResult(code, success, msg, data)
+
+ /**
+ * Build response result object
+ *
+ * @param code Response code object
+ * @param success Is successful
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseCode
+ */
+ fun build(code: ResponseCode, success: Boolean, msg: String, data: T?) =
+ build(code.code, success, msg, data)
+
+ /**
+ * Build successful response result object
+ *
+ * @param code Response code object
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseCode
+ */
+ fun success(code: ResponseCode = ResponseCode.SYSTEM_OK, msg: String = "success", data: T? = null) =
+ build(code, true, msg, data)
+
+ /**
+ * Build failure response result object
+ *
+ * @param code Response code object
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseCode
+ */
+ fun fail(code: ResponseCode = ResponseCode.SYSTEM_ERROR, msg: String = "fail", data: T? = null) =
+ build(code, false, msg, data)
+
+ /**
+ * Build database successful response result object
+ *
+ * @param code Response code object
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseCode
+ */
+ fun databaseSuccess(
+ code: ResponseCode = ResponseCode.DATABASE_SELECT_SUCCESS, msg: String = "success", data: T? = null
+ ) = build(code, true, msg, data)
+
+ /**
+ * Build database failure response result object
+ *
+ * @param code Response code object
+ * @param msg Response message
+ * @param data Response data
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ResponseCode
+ */
+ fun databaseFail(
+ code: ResponseCode = ResponseCode.DATABASE_SELECT_FAILED, msg: String = "fail", data: T? = null
+ ) = build(code, false, msg, data)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Func.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Func.kt
new file mode 100644
index 0000000..61fa701
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Func.kt
@@ -0,0 +1,55 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Function entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_func")
+class Func : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Parent ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("parent_id")
+ var parentId: Long? = null
+
+ /**
+ * Menu ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("menu_id")
+ var menuId: Long? = null
+
+ override fun toString(): String {
+ return "Func(id=$id, name=$name, parentId=$parentId, menuId=$menuId)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Group.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Group.kt
new file mode 100644
index 0000000..5d779ad
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Group.kt
@@ -0,0 +1,95 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Group entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_group")
+class Group : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Enable
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * Role list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ */
+ @TableField(exist = false)
+ var roles: List? = null
+
+ override fun toString(): String {
+ return "Group(id=$id, name=$name, enable=$enable, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, roles=$roles)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/LoginUser.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/LoginUser.kt
new file mode 100644
index 0000000..29fafce
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/LoginUser.kt
@@ -0,0 +1,67 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+/**
+ * Login user entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserDetails
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+class LoginUser() : UserDetails {
+ /**
+ * User object
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ lateinit var user: User
+
+ @JsonIgnore
+ private var authorities: List? = null
+
+ constructor(user: User) : this() {
+ this.user = user
+ }
+
+ @JsonIgnore
+ override fun getAuthorities(): List {
+ authorities?.let { return it }
+ authorities = user.operations?.map { SimpleGrantedAuthority(it.code) } ?: emptyList()
+
+ return authorities as List
+ }
+
+ @JsonIgnore
+ override fun getPassword() = user.password
+
+ @JsonIgnore
+ override fun getUsername() = user.username
+
+ @JsonIgnore
+ override fun isAccountNonExpired() =
+ user.expiration == null || user.expiration!!.isAfter(LocalDateTime.now(ZoneOffset.UTC))
+
+ @JsonIgnore
+ override fun isAccountNonLocked() = user.locking == 0
+
+ @JsonIgnore
+ override fun isCredentialsNonExpired() =
+ user.credentialsExpiration == null || user.credentialsExpiration!!.isAfter(LocalDateTime.now(ZoneOffset.UTC))
+
+ @JsonIgnore
+ override fun isEnabled() = user.enable == 1
+
+ override fun toString(): String {
+ return "LoginUser(user=$user, authorities=$authorities)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Menu.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Menu.kt
new file mode 100644
index 0000000..d81e235
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Menu.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Menu entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_menu")
+class Menu : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * URL
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("url")
+ var url: String? = null
+
+ /**
+ * Parent ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("parent_id")
+ var parentId: Long? = null
+
+ /**
+ * Module ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("module_id")
+ var moduleId: Long? = null
+
+ override fun toString(): String {
+ return "Menu(id=$id, name=$name, url=$url, parentId=$parentId, moduleId=$moduleId)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Module.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Module.kt
new file mode 100644
index 0000000..fa26a28
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Module.kt
@@ -0,0 +1,37 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Module Entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_module")
+class Module : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ override fun toString(): String {
+ return "Module(id=$id, name=$name)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Operation.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Operation.kt
new file mode 100644
index 0000000..c111ce6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Operation.kt
@@ -0,0 +1,55 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Operation entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_operation")
+class Operation : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Code
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("code")
+ var code: String? = null
+
+ /**
+ * Function ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("func_id")
+ var funcId: Long? = null
+
+ override fun toString(): String {
+ return "Operation(id=$id, name=$name, code=$code, funcId=$funcId)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Power.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Power.kt
new file mode 100644
index 0000000..4f1a7a0
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Power.kt
@@ -0,0 +1,37 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Power entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_power")
+class Power : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Type ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("type_id")
+ var typeId: Int? = null
+
+ override fun toString(): String {
+ return "Power(id=$id, typeId=$typeId)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerSet.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerSet.kt
new file mode 100644
index 0000000..c99f5ae
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerSet.kt
@@ -0,0 +1,51 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import java.io.Serializable
+
+/**
+ * Set of power entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+class PowerSet : Serializable {
+ /**
+ * Module list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Module
+ */
+ var moduleList: List? = null
+
+ /**
+ * Menu list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Menu
+ */
+ var menuList: List? = null
+
+ /**
+ * Function list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Func
+ */
+ var funcList: List? = null
+
+ /**
+ * Operation list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Operation
+ */
+ var operationList: List? = null
+
+ override fun toString(): String {
+ return "PowerSet(moduleList=$moduleList, menuList=$menuList, funcList=$funcList, operationList=$operationList)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerType.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerType.kt
new file mode 100644
index 0000000..c2be1e4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/PowerType.kt
@@ -0,0 +1,37 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import java.io.Serializable
+
+/**
+ * Power type entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_power_type")
+class PowerType : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ override fun toString(): String {
+ return "PowerType(id=$id, name=$name)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RPowerRole.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RPowerRole.kt
new file mode 100644
index 0000000..fc0da02
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RPowerRole.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+/**
+ * Power role intermediate entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_r_power_role")
+class RPowerRole : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Power ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("power_id")
+ var powerId: Long? = null
+
+ /**
+ * Role ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("role_id")
+ var roleId: Long? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "RPowerRole(id=$id, powerId=$powerId, roleId=$roleId, deleted=$deleted, version=$version)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RRoleGroup.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RRoleGroup.kt
new file mode 100644
index 0000000..a3af844
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RRoleGroup.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+/**
+ * Role group intermediate entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_r_role_group")
+class RRoleGroup : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Role ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("role_id")
+ var roleId: Long? = null
+
+ /**
+ * Group ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("group_id")
+ var groupId: Long? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "RRoleGroup(id=$id, roleId=$roleId, groupId=$groupId, deleted=$deleted, version=$version)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserGroup.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserGroup.kt
new file mode 100644
index 0000000..60986f3
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserGroup.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+/**
+ * User group intermediate entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_r_user_group")
+class RUserGroup : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * User ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("user_id")
+ var userId: Long? = null
+
+ /**
+ * Group ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("group_id")
+ var groupId: Long? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "RUserGroup(id=$id, userId=$userId, groupId=$groupId, deleted=$deleted, version=$version)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserRole.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserRole.kt
new file mode 100644
index 0000000..a3f7d77
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/RUserRole.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+/**
+ * User role intermediate entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_r_user_role")
+class RUserRole : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * User ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("user_id")
+ var userId: Long? = null
+
+ /**
+ * Role ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("role_id")
+ var roleId: Long? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "RUserRole(id=$id, userId=$userId, roleId=$roleId, deleted=$deleted, version=$version)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Role.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Role.kt
new file mode 100644
index 0000000..e5175fc
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/Role.kt
@@ -0,0 +1,135 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Role entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_role")
+class Role : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Enable
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * Module list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Module
+ */
+ @TableField(exist = false)
+ var modules: List? = null
+
+ /**
+ * Menu list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Menu
+ */
+ @TableField(exist = false)
+ var menus: List? = null
+
+ /**
+ * Function list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Func
+ */
+ @TableField(exist = false)
+ var funcs: List? = null
+
+ /**
+ * Operation list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Operation
+ */
+ @TableField(exist = false)
+ var operations: List? = null
+
+ /**
+ * Power list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Power
+ */
+ @TableField(exist = false)
+ var powers: List? = null
+
+ override fun toString(): String {
+ return "Role(id=$id, name=$name, enable=$enable, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations, powers=$powers)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt
new file mode 100644
index 0000000..93512fb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt
@@ -0,0 +1,265 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * User entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_user")
+class User() : Serializable {
+ constructor(id: Long?, username: String, password: String, enable: Boolean = true) : this() {
+ this.id = id
+ this.username = username
+ this.password = password
+ this.enable = if (enable) 1 else 0
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Username
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("username")
+ var username: String? = null
+
+ /**
+ * Password
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("password")
+ var password: String? = null
+
+ /**
+ * Two-factor
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("two_factor")
+ var twoFactor: String? = null
+
+ /**
+ * Verify email
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("verify")
+ var verify: String? = null
+
+ /**
+ * Forget password
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("forget")
+ var forget: String? = null
+
+ /**
+ * Locking
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("locking")
+ var locking: Int? = null
+
+ /**
+ * Expiration time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("expiration")
+ var expiration: LocalDateTime? = null
+
+ /**
+ * Credentials expiration time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("credentials_expiration")
+ var credentialsExpiration: LocalDateTime? = null
+
+ /**
+ * Enable
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ /**
+ * Current login time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("current_login_time")
+ var currentLoginTime: LocalDateTime? = null
+
+ /**
+ * Current login IP
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("current_login_ip")
+ var currentLoginIp: String? = null
+
+ /**
+ * Last login time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("last_login_time")
+ var lastLoginTime: LocalDateTime? = null
+
+ /**
+ * Last login IP
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("last_login_ip")
+ var lastLoginIp: String? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time")
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time")
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * User information
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see UserInfo
+ */
+ @TableField(exist = false)
+ var userInfo: UserInfo? = null
+
+ /**
+ * Role list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ */
+ @TableField(exist = false)
+ var roles: List? = null
+
+ /**
+ * Group list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Group
+ */
+ @TableField(exist = false)
+ var groups: List? = null
+
+ /**
+ * Module list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Module
+ */
+ @TableField(exist = false)
+ var modules: List? = null
+
+ /**
+ * Menu list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Menu
+ */
+ @TableField(exist = false)
+ var menus: List? = null
+
+ /**
+ * Function list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Func
+ */
+ @TableField(exist = false)
+ var funcs: List? = null
+
+ /**
+ * Operation list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Operation
+ */
+ @TableField(exist = false)
+ var operations: List? = null
+
+ override fun toString(): String {
+ return "User(id=$id, username=$username, password=$password, twoFactor=$twoFactor, verify=$verify, forget=$forget, locking=$locking, expiration=$expiration, credentialsExpiration=$credentialsExpiration, enable=$enable, currentLoginTime=$currentLoginTime, currentLoginIp=$currentLoginIp, lastLoginTime=$lastLoginTime, lastLoginIp=$lastLoginIp, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, userInfo=$userInfo, roles=$roles, groups=$groups, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/UserInfo.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/UserInfo.kt
new file mode 100644
index 0000000..8aa8e24
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/UserInfo.kt
@@ -0,0 +1,103 @@
+package top.fatweb.oxygen.api.entity.permission
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * User information entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_user_info")
+class UserInfo : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * User ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("user_id")
+ var userId: Long? = null
+
+ /**
+ * Nickname
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("nickname")
+ var nickname: String? = null
+
+ /**
+ * Avatar in base64
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("avatar")
+ var avatar: String? = null
+
+ /**
+ * Email
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("email")
+ var email: String? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "UserInfo(id=$id, userId=$userId, nickname=$nickname, avatar=$avatar, email=$email, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/system/EventLog.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/EventLog.kt
new file mode 100644
index 0000000..c689547
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/EventLog.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.system
+
+import com.baomidou.mybatisplus.annotation.EnumValue
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonValue
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Event log entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_l_event_log")
+class EventLog : Serializable {
+ enum class Event(@field:EnumValue @field:JsonValue val code: String) {
+ LOGIN("LOGIN"), LOGOUT("LOGOUT"), REGISTER("REGISTER"), VERIFY("VERIFY"), API("API")
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Event
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("event")
+ var event: Event? = null
+
+ /**
+ * Operate user ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("operate_user_id")
+ var operateUserId: Long? = null
+
+ /**
+ * Operate time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+ @TableField("operate_time")
+ var operateTime: LocalDateTime? = null
+
+ override fun toString(): String {
+ return "EventLog(id=$id, event=$event, operateUserId=$operateUserId, operateTime=$operateTime)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SensitiveWord.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SensitiveWord.kt
new file mode 100644
index 0000000..a024d6e
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SensitiveWord.kt
@@ -0,0 +1,63 @@
+package top.fatweb.oxygen.api.entity.system
+
+import com.baomidou.mybatisplus.annotation.EnumValue
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler
+import com.fasterxml.jackson.annotation.JsonValue
+import java.io.Serializable
+
+/**
+ * Sensitive word entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_s_sensitive_word", autoResultMap = true)
+class SensitiveWord : Serializable {
+ enum class Use(@field:EnumValue @field:JsonValue val code: String) {
+ USERNAME("USERNAME"), TITLE("TITLE");
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Word
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("word")
+ var word: String? = null
+
+ /**
+ * Use for
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(value = "use_for", typeHandler = JacksonTypeHandler::class)
+ @JvmField
+ var useFor: Set? = null
+
+ /**
+ * Enable
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ override fun toString(): String {
+ return "SensitiveWord(id=$id, word=$word, useFor=$useFor, enable=$enable)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/system/StatisticsLog.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/StatisticsLog.kt
new file mode 100644
index 0000000..165a2c4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/StatisticsLog.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.system
+
+import com.baomidou.mybatisplus.annotation.EnumValue
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonValue
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Statistics log entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_l_statistics_log")
+class StatisticsLog : Serializable {
+ enum class KeyItem(@field:EnumValue @field:JsonValue val code: String) {
+ ONLINE_USERS_COUNT("ONLINE_USER_COUNT")
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Key
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("key")
+ var key: KeyItem? = null
+
+ /**
+ * Value
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("value")
+ var value: String? = null
+
+ /**
+ * Record time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+ @TableField("record_time")
+ var recordTime: LocalDateTime? = null
+
+ override fun toString(): String {
+ return "StatisticsLog(id=$id, key=$key, value=$value, recordTime=$recordTime)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SysLog.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SysLog.kt
new file mode 100644
index 0000000..b17e2fa
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/system/SysLog.kt
@@ -0,0 +1,186 @@
+package top.fatweb.oxygen.api.entity.system
+
+import com.baomidou.mybatisplus.annotation.EnumValue
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonValue
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * System log entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_l_sys_log")
+class SysLog : Serializable {
+ /**
+ * Log type enum
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ enum class LogType(@field:EnumValue @field:JsonValue val code: String) {
+ INFO("INFO"), ERROR("ERROR"), LOGIN("LOGIN"), LOGOUT("LOGOUT"), REGISTER("REGISTER"), STATISTICS("STATISTICS"), API(
+ "API"
+ )
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Log type
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LogType
+ */
+ @TableField("log_type")
+ var logType: LogType? = null
+
+ /**
+ * Operate user ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("operate_user_id")
+ var operateUserId: Long? = null
+
+ /**
+ * Operate time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+ @TableField("operate_time")
+ var operateTime: LocalDateTime? = null
+
+ /**
+ * Request URI
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("request_uri")
+ var requestUri: String? = null
+
+ /**
+ * Request method
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("request_method")
+ var requestMethod: String? = null
+
+ /**
+ * Request parameters
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("request_params")
+ var requestParams: String? = null
+
+ /**
+ * Request IP
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("request_ip")
+ var requestIp: String? = null
+
+ /**
+ * Request server address
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("request_server_address")
+ var requestServerAddress: String? = null
+
+ /**
+ * Is exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("exception")
+ var exception: Int? = null
+
+ /**
+ * Exception information
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("exception_info")
+ var exceptionInfo: String? = null
+
+ /**
+ * Start time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+ @TableField("start_time")
+ var startTime: LocalDateTime? = null
+
+ /**
+ * End time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+ @TableField("end_time")
+ var endTime: LocalDateTime? = null
+
+ /**
+ * Execute time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("execute_time")
+ var executeTime: Long? = null
+
+ /**
+ * User agent
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("user_agent")
+ var userAgent: String? = null
+
+ /**
+ * Operate username
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var operateUsername: String? = null
+
+ override fun toString(): String {
+ return "SysLog(id=$id, logType=$logType, operateUserId=$operateUserId, operateTime=$operateTime, requestUri=$requestUri, requestMethod=$requestMethod, requestParams=$requestParams, requestIp=$requestIp, requestServerAddress=$requestServerAddress, exception=$exception, exceptionInfo=$exceptionInfo, startTime=$startTime, endTime=$endTime, executeTime=$executeTime, userAgent=$userAgent, operateUsername=$operateUsername)"
+ }
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Platform.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Platform.kt
new file mode 100644
index 0000000..f959266
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Platform.kt
@@ -0,0 +1,14 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.EnumValue
+import com.fasterxml.jackson.annotation.JsonValue
+
+/**
+ * Platform enum
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+enum class Platform(@field:EnumValue @field:JsonValue val code: String) {
+ WEB("WEB"), DESKTOP("DESKTOP"), ANDROID("ANDROID")
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/RToolCategory.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/RToolCategory.kt
new file mode 100644
index 0000000..cce2436
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/RToolCategory.kt
@@ -0,0 +1,64 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+/**
+ * Tool category intermediate entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_r_tool_main_category")
+class RToolCategory : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Tool ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("tool_id")
+ var toolId: Long? = null
+
+ /**
+ * Category ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("category_id")
+ var categoryId: Long? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "RToolCategory(id=$id, toolId=$toolId, categoryId=$categoryId, deleted=$deleted, version=$version)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Tool.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Tool.kt
new file mode 100644
index 0000000..bef4466
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/Tool.kt
@@ -0,0 +1,262 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler
+import com.fasterxml.jackson.annotation.JsonValue
+import top.fatweb.oxygen.api.entity.permission.User
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Tool entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_b_tool_main", autoResultMap = true)
+class Tool : Serializable {
+ /**
+ * Tool review type enum
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ enum class ReviewType(@field:EnumValue @field:JsonValue val code: String) {
+ NONE("NONE"), PROCESSING("PROCESSING"), PASS("PASS"), REJECT("REJECT")
+ }
+
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Tool ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("tool_id")
+ var toolId: String? = null
+
+ /**
+ * Icon
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("icon")
+ var icon: String? = null
+
+ /**
+ * Platform
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Platform
+ */
+ @TableField("platform")
+ var platform: Platform? = null
+
+ /**
+ * Description
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("description")
+ var description: String? = null
+
+ /**
+ * Base ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("base_id")
+ var baseId: Long? = null
+
+ /**
+ * Author ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("author_id")
+ var authorId: Long? = null
+
+ /**
+ * Version of tool
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("ver")
+ var ver: String? = null
+
+ /**
+ * Keywords
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("keywords", typeHandler = JacksonTypeHandler::class)
+ var keywords: List? = null
+
+ /**
+ * Source code ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("source_id")
+ var sourceId: Long? = null
+
+ /**
+ * Compile product ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("dist_id")
+ var distId: Long? = null
+
+ /**
+ * Entry point
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("entry_point")
+ var entryPoint: String? = null
+
+ /**
+ * Publish
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("publish")
+ var publish: Long? = null
+
+ /**
+ * Review
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ReviewType
+ */
+ @TableField("review")
+ var review: ReviewType? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * Author
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var author: User? = null
+
+ /**
+ * Base
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var base: ToolBase? = null
+
+ /**
+ * Categories
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var categories: List? = null
+
+ /**
+ * Source
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var source: ToolData? = null
+
+ /**
+ * Dist
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var dist: ToolData? = null
+
+ /**
+ * Favorite
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var favorite: Int? = null
+
+ override fun toString(): String {
+ return "Tool(id=$id, name=$name, toolId=$toolId, icon=$icon, platform=$platform, description=$description, baseId=$baseId, authorId=$authorId, ver=$ver, keywords=$keywords, sourceId=$sourceId, distId=$distId, entryPoint=$entryPoint, publish=$publish, review=$review, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, author=$author, base=$base, categories=$categories, source=$source, dist=$dist, favorite=$favorite)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolBase.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolBase.kt
new file mode 100644
index 0000000..a88745b
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolBase.kt
@@ -0,0 +1,140 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Tool base entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_b_tool_base")
+class ToolBase : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Source ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("source_id")
+ var sourceId: Long? = null
+
+ /**
+ * Dist ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("dist_id")
+ var distId: Long? = null
+
+ /**
+ * Platform
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Platform
+ */
+ @TableField("platform")
+ var platform: Platform? = null
+
+ /**
+ * Has compiled
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("compiled")
+ var compiled: Int? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * Source
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var source: ToolData? = null
+
+ /**
+ * Dist
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var dist: ToolData? = null
+
+ /**
+ * Dist data
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var distData: String? = null
+
+ override fun toString(): String {
+ return "ToolBase(id=$id, name=$name, sourceId=$sourceId, distId=$distId, platform=$platform, compiled=$compiled, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, source=$source, dist=$dist, distData=$distData)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolCategory.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolCategory.kt
new file mode 100644
index 0000000..37246b1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolCategory.kt
@@ -0,0 +1,85 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Tool category entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_b_tool_category")
+class ToolCategory : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Enabel
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "ToolCategory(id=$id, name=$name, enable=$enable, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolData.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolData.kt
new file mode 100644
index 0000000..dd1f0dd
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolData.kt
@@ -0,0 +1,76 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Tool data entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_b_tool_data")
+class ToolData : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Data
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("data")
+ var data: String? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "ToolData(id=$id, data=$data, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolFavorite.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolFavorite.kt
new file mode 100644
index 0000000..1d474e6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolFavorite.kt
@@ -0,0 +1,67 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+
+@TableName("t_b_tool_favorite")
+class ToolFavorite : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * User ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("user_id")
+ var userId: Long? = null
+
+ /**
+ * Author ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("author_id")
+ var authorId: Long? = null
+
+ /**
+ * Tool ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("tool_id")
+ var toolId: String? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ override fun toString(): String {
+ return "ToolFavorite(id=$id, userId=$userId, authorId=$authorId, toolId=$toolId, deleted=$deleted, version=$version)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolTemplate.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolTemplate.kt
new file mode 100644
index 0000000..b2b5007
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/tool/ToolTemplate.kt
@@ -0,0 +1,140 @@
+package top.fatweb.oxygen.api.entity.tool
+
+import com.baomidou.mybatisplus.annotation.*
+import java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Tool template entity
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@TableName("t_b_tool_template")
+class ToolTemplate : Serializable {
+ /**
+ * ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableId("id")
+ var id: Long? = null
+
+ /**
+ * Name
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("name")
+ var name: String? = null
+
+ /**
+ * Base ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("base_id")
+ var baseId: Long? = null
+
+ /**
+ * Source ID
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("source_id")
+ var sourceId: Long? = null
+
+ /**
+ * Platform
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Platform
+ */
+ @TableField("platform")
+ var platform: Platform? = null
+
+ /**
+ * Entry point
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("entry_point")
+ var entryPoint: String? = null
+
+ /**
+ * Enable
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("enable")
+ var enable: Int? = null
+
+ /**
+ * Create time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("create_time", fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ /**
+ * Update time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see LocalDateTime
+ */
+ @TableField("update_time", fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ /**
+ * Deleted
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("deleted")
+ @TableLogic
+ var deleted: Long? = null
+
+ /**
+ * Version
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField("version")
+ @Version
+ var version: Int? = null
+
+ /**
+ * Source
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var source: ToolData? = null
+
+ /**
+ * Base
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @TableField(exist = false)
+ var base: ToolBase? = null
+
+ override fun toString(): String {
+ return "ToolTemplate(id=$id, name=$name, baseId=$baseId, sourceId=$sourceId, platform=$platform, entryPoint=$entryPoint, enable=$enable, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, source=$source, base=$base)"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedInitException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedInitException.kt
new file mode 100644
index 0000000..311a9a6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedInitException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Account need initialize exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class AccountNeedInitException : RuntimeException("Account need initialize")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedResetPasswordException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedResetPasswordException.kt
new file mode 100644
index 0000000..756682b
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/AccountNeedResetPasswordException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Account need reset password exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class AccountNeedResetPasswordException : RuntimeException("Account need reset password")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt
new file mode 100644
index 0000000..f9339cc
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Already has two-factor exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class AlreadyHasTwoFactorException : RuntimeException("Already has two-factor")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseDeleteException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseDeleteException.kt
new file mode 100644
index 0000000..cd1ea34
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseDeleteException.kt
@@ -0,0 +1,11 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Database delete exception
+ *
+ * @param message Exception message
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class DatabaseDeleteException(message: String = "Database delete failed"): RuntimeException(message)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseInsertException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseInsertException.kt
new file mode 100644
index 0000000..b3e301d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseInsertException.kt
@@ -0,0 +1,11 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Database insert exception
+ *
+ * @param message Exception message
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class DatabaseInsertException(message: String = "Database insert failed"): RuntimeException(message)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseSelectException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseSelectException.kt
new file mode 100644
index 0000000..1b5623b
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseSelectException.kt
@@ -0,0 +1,11 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Database select exception
+ *
+ * @param message Exception message
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class DatabaseSelectException(message: String = "Database select failed"): RuntimeException(message)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseUpdateException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseUpdateException.kt
new file mode 100644
index 0000000..43f6c09
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/DatabaseUpdateException.kt
@@ -0,0 +1,11 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Database update exception
+ *
+ * @param message Exception message
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class DatabaseUpdateException(message: String = "Database update failed"): RuntimeException(message)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/IllegalVersionException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/IllegalVersionException.kt
new file mode 100644
index 0000000..b53fc99
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/IllegalVersionException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Illegal version exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class IllegalVersionException : RuntimeException("Illegal Version")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt
new file mode 100644
index 0000000..6e2b6a5
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Invalid captcha code exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class InvalidCaptchaCodeException : RuntimeException("Invalid captcha code")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/MatchSensitiveWordException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/MatchSensitiveWordException.kt
new file mode 100644
index 0000000..dbf5cf6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/MatchSensitiveWordException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Match sensitive word exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class MatchSensitiveWordException : RuntimeException("Match sensitive word")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt
new file mode 100644
index 0000000..fb51ca4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Need two-factor code exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class NeedTwoFactorException : RuntimeException("Need two-factor code")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NoEmailConfigException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoEmailConfigException.kt
new file mode 100644
index 0000000..a7bd6f2
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoEmailConfigException.kt
@@ -0,0 +1,13 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Email settings not configured exception
+ *
+ * @param configs Configs not config
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class NoEmailConfigException(
+ vararg configs: String
+) : RuntimeException("Email settings not configured: ${configs.joinToString(", ")}")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NoRecordFoundException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoRecordFoundException.kt
new file mode 100644
index 0000000..65274fa
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoRecordFoundException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * No record found exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class NoRecordFoundException : RuntimeException("No record found")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt
new file mode 100644
index 0000000..c739da1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * No two-factor found exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class NoTwoFactorFoundException : RuntimeException("No two-factor found")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NoVerificationRequiredException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoVerificationRequiredException.kt
new file mode 100644
index 0000000..c0bcc3a
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoVerificationRequiredException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * No verification required exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class NoVerificationRequiredException : RuntimeException("No verification required")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/RecordAlreadyExists.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/RecordAlreadyExists.kt
new file mode 100644
index 0000000..20aaf88
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/RecordAlreadyExists.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Record already exists exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class RecordAlreadyExists : RuntimeException("Record already exists")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequentException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequentException.kt
new file mode 100644
index 0000000..59a5b16
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequentException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Request too frequent exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class RequestTooFrequentException: RuntimeException("Request too frequent")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/RetrieveCodeErrorOrExpiredException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/RetrieveCodeErrorOrExpiredException.kt
new file mode 100644
index 0000000..d8010c1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/RetrieveCodeErrorOrExpiredException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Retrieve code error or expired exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class RetrieveCodeErrorOrExpiredException : RuntimeException("Retrieve code error or expired")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/TokenHasExpiredException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/TokenHasExpiredException.kt
new file mode 100644
index 0000000..7250ef7
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/TokenHasExpiredException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Token has expired exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class TokenHasExpiredException : RuntimeException("Token has expired")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasBeenPublishedException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasBeenPublishedException.kt
new file mode 100644
index 0000000..f80dfca
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasBeenPublishedException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Tool has been published exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class ToolHasBeenPublishedException : RuntimeException("Tool has been published")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasNotBeenPublishedException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasNotBeenPublishedException.kt
new file mode 100644
index 0000000..af82607
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasNotBeenPublishedException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Tool has not been published exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class ToolHasNotBeenPublishedException : RuntimeException("Tool has not been published")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasUnpublishedVersionException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasUnpublishedVersionException.kt
new file mode 100644
index 0000000..0b4d41e
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolHasUnpublishedVersionException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Tool has unpublished version exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class ToolHasUnpublishedVersionException : RuntimeException("Has unpublished version")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolNotUnderReviewException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolNotUnderReviewException.kt
new file mode 100644
index 0000000..586d71f
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolNotUnderReviewException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Tool not under review exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class ToolNotUnderReviewException : RuntimeException("Tool not under review")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolUnderReviewException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolUnderReviewException.kt
new file mode 100644
index 0000000..83083f0
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/ToolUnderReviewException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Tool under review exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class ToolUnderReviewException : RuntimeException("Tool under review")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt
new file mode 100644
index 0000000..e93a68d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Two-factor verification code error exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class TwoFactorVerificationCodeErrorException : RuntimeException("Two-factor verification code error")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/UserNotFoundException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/UserNotFoundException.kt
new file mode 100644
index 0000000..6b2dc60
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/UserNotFoundException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * User not found exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class UserNotFoundException : RuntimeException("User not found")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/VerificationCodeErrorOrExpiredException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/VerificationCodeErrorOrExpiredException.kt
new file mode 100644
index 0000000..bfbbee7
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/VerificationCodeErrorOrExpiredException.kt
@@ -0,0 +1,10 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Verification code error or expired exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RuntimeException
+ */
+class VerificationCodeErrorOrExpiredException : RuntimeException("Verification code is error or has expired")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/filter/ExceptionFilter.kt b/src/main/kotlin/top/fatweb/oxygen/api/filter/ExceptionFilter.kt
new file mode 100644
index 0000000..6117074
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/filter/ExceptionFilter.kt
@@ -0,0 +1,28 @@
+package top.fatweb.oxygen.api.filter
+
+import jakarta.servlet.Filter
+import jakarta.servlet.FilterChain
+import jakarta.servlet.ServletRequest
+import jakarta.servlet.ServletResponse
+import org.springframework.stereotype.Component
+
+/**
+ * Exception filter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Filter
+ */
+@Component
+class ExceptionFilter : Filter {
+ override fun doFilter(
+ servletRequest: ServletRequest?, servletResponse: ServletResponse?, filterChain: FilterChain?
+ ) {
+ try {
+ filterChain!!.doFilter(servletRequest, servletResponse)
+ } catch (e: Exception) {
+ servletRequest?.setAttribute("filter.error", e)
+ servletRequest?.getRequestDispatcher("/error/thrown")?.forward(servletRequest, servletResponse)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/filter/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/top/fatweb/oxygen/api/filter/JwtAuthenticationTokenFilter.kt
new file mode 100644
index 0000000..a103133
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/filter/JwtAuthenticationTokenFilter.kt
@@ -0,0 +1,57 @@
+package top.fatweb.oxygen.api.filter
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.stereotype.Component
+import org.springframework.util.StringUtils
+import org.springframework.web.filter.OncePerRequestFilter
+import top.fatweb.oxygen.api.entity.permission.LoginUser
+import top.fatweb.oxygen.api.exception.TokenHasExpiredException
+import top.fatweb.oxygen.api.properties.SecurityProperties
+import top.fatweb.oxygen.api.util.JwtUtil
+import top.fatweb.oxygen.api.util.RedisUtil
+import top.fatweb.oxygen.api.util.WebUtil
+
+/**
+ * Jwt authentication token filter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see RedisUtil
+ * @see OncePerRequestFilter
+ */
+@Component
+class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() {
+ override fun doFilterInternal(
+ request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain
+ ) {
+ val tokenWithPrefix = request.getHeader(SecurityProperties.headerKey)
+
+ if (!StringUtils.hasText(tokenWithPrefix) || "/error/thrown" == request.servletPath) {
+ filterChain.doFilter(request, response)
+ return
+ }
+
+ val token = WebUtil.getToken(tokenWithPrefix)
+ JwtUtil.parseJwt(token)
+
+ val redisKeyPattern = "${SecurityProperties.jwtIssuer}_login_*:${token}"
+ val redisKeys = redisUtil.keys(redisKeyPattern)
+ if (redisKeys.isEmpty()) {
+ throw TokenHasExpiredException()
+ }
+
+ val loginUser = redisUtil.getObject(redisKeys.first())
+ loginUser ?: throw TokenHasExpiredException()
+
+ redisUtil.setExpire(redisKeys.first(), SecurityProperties.redisTtl, SecurityProperties.redisTtlUnit)
+
+ val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities)
+ SecurityContextHolder.getContext().authentication = authenticationToken
+
+ filterChain.doFilter(request, response)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/handler/DataMetaObjectHandler.kt b/src/main/kotlin/top/fatweb/oxygen/api/handler/DataMetaObjectHandler.kt
new file mode 100644
index 0000000..e667eec
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/DataMetaObjectHandler.kt
@@ -0,0 +1,26 @@
+package top.fatweb.oxygen.api.handler
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
+import org.apache.ibatis.reflection.MetaObject
+import org.springframework.stereotype.Component
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+/**
+ * Date meta object handler
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see MetaObjectHandler
+ */
+@Component
+class DataMetaObjectHandler : MetaObjectHandler {
+ override fun insertFill(metaObject: MetaObject?) {
+ this.strictInsertFill(metaObject, "createTime", LocalDateTime::class.java, LocalDateTime.now(ZoneOffset.UTC))
+ this.strictInsertFill(metaObject, "updateTime", LocalDateTime::class.java, LocalDateTime.now(ZoneOffset.UTC))
+ }
+
+ override fun updateFill(metaObject: MetaObject?) {
+ this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::class.java, LocalDateTime.now(ZoneOffset.UTC))
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
new file mode 100644
index 0000000..587f5a1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
@@ -0,0 +1,287 @@
+package top.fatweb.oxygen.api.handler
+
+import com.auth0.jwt.exceptions.JWTDecodeException
+import com.auth0.jwt.exceptions.SignatureVerificationException
+import com.auth0.jwt.exceptions.TokenExpiredException
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.dao.DuplicateKeyException
+import org.springframework.http.converter.HttpMessageNotReadableException
+import org.springframework.jdbc.BadSqlGrammarException
+import org.springframework.jdbc.UncategorizedSQLException
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.authentication.*
+import org.springframework.web.HttpRequestMethodNotSupportedException
+import org.springframework.web.bind.MethodArgumentNotValidException
+import org.springframework.web.bind.annotation.ExceptionHandler
+import org.springframework.web.bind.annotation.RestControllerAdvice
+import org.springframework.web.servlet.resource.NoResourceFoundException
+import top.fatweb.avatargenerator.AvatarException
+import top.fatweb.oxygen.api.entity.common.ResponseCode
+import top.fatweb.oxygen.api.entity.common.ResponseResult
+import top.fatweb.oxygen.api.exception.*
+
+/**
+ * Exception handler
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@RestControllerAdvice
+class ExceptionHandler {
+ private val logger: Logger = LoggerFactory.getLogger(this::class.java)
+
+ /**
+ * Handle all exception
+ *
+ * @param e Exception
+ * @return Response object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Exception
+ * @see ResponseResult
+ */
+ @ExceptionHandler(value = [Exception::class])
+ fun exceptionHandler(e: Exception): ResponseResult<*> {
+ return when (e) {
+ /* Request */
+ is HttpRequestMethodNotSupportedException, is NoResourceFoundException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_ILLEGAL, e.localizedMessage, null)
+ }
+
+ is HttpMessageNotReadableException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_ILLEGAL, e.localizedMessage.split(":")[0], null)
+ }
+
+ is MethodArgumentNotValidException -> {
+ logger.debug(e.localizedMessage, e)
+ val errorMessage = e.allErrors.map { error -> error.defaultMessage }.joinToString(". ")
+ ResponseResult.fail(ResponseCode.SYSTEM_ARGUMENT_NOT_VALID, errorMessage, null)
+ }
+
+ is RequestTooFrequentException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_TOO_FREQUENT, e.localizedMessage, null)
+ }
+
+ /* Authentication */
+ is InsufficientAuthenticationException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_UNAUTHORIZED, e.localizedMessage, null)
+ }
+
+ is LockedException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_USER_LOCKED, "User account has been locked", null)
+ }
+
+ is AccountExpiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_USER_EXPIRED, "User account has expired", null)
+ }
+
+ is CredentialsExpiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(
+ ResponseCode.PERMISSION_USER_CREDENTIALS_EXPIRED,
+ "User credentials have expired",
+ null
+ )
+ }
+
+ is DisabledException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_USER_DISABLE, "User has been disabled", null)
+ }
+
+ is TokenExpiredException, is TokenHasExpiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_TOKEN_HAS_EXPIRED, e.localizedMessage, null)
+ }
+
+ is InternalAuthenticationServiceException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_USERNAME_NOT_FOUND, "Username not found", null)
+ }
+
+ is BadCredentialsException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(
+ ResponseCode.PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
+ "Wrong user name or password",
+ null
+ )
+ }
+
+ is SignatureVerificationException, is JWTDecodeException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_TOKEN_ILLEGAL, "Token illegal", null)
+ }
+
+ is AccessDeniedException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_ACCESS_DENIED, "Access Denied", null)
+ }
+
+ is UserNotFoundException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_USER_NOT_FOUND, e.localizedMessage, null)
+ }
+
+ is NoVerificationRequiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_NO_VERIFICATION_REQUIRED, e.localizedMessage, null)
+ }
+
+ is VerificationCodeErrorOrExpiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_VERIFY_CODE_ERROR_OR_EXPIRED, e.localizedMessage, null)
+ }
+
+ is AccountNeedInitException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_INIT, e.localizedMessage, null)
+ }
+
+ is RetrieveCodeErrorOrExpiredException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED, e.localizedMessage, null)
+ }
+
+ is AccountNeedResetPasswordException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_RESET_PASSWORD, e.localizedMessage, null)
+ }
+
+ is InvalidCaptchaCodeException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_INVALID_CAPTCHA_CODE, e.localizedMessage, null)
+ }
+
+ is NeedTwoFactorException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_NEED_TWO_FACTOR, e.localizedMessage, null)
+ }
+
+ is AlreadyHasTwoFactorException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_ALREADY_HAS_TWO_FACTOR, e.localizedMessage, null)
+ }
+
+ is NoTwoFactorFoundException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.PERMISSION_NO_TWO_FACTOR_FOUND, e.localizedMessage, null)
+ }
+
+ is TwoFactorVerificationCodeErrorException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(
+ ResponseCode.PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR,
+ e.localizedMessage,
+ null
+ )
+ }
+
+ /* SQL */
+ is DatabaseSelectException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.databaseFail(ResponseCode.DATABASE_SELECT_FAILED, e.localizedMessage, null)
+ }
+
+ is DatabaseInsertException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.databaseFail(ResponseCode.DATABASE_INSERT_FAILED, e.localizedMessage, null)
+ }
+
+ is DatabaseUpdateException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.databaseFail(ResponseCode.DATABASE_UPDATE_FAILED, e.localizedMessage, null)
+ }
+
+ is DatabaseDeleteException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED, e.localizedMessage, null)
+ }
+
+ is BadSqlGrammarException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, "Incorrect SQL syntax", null)
+ }
+
+ is DuplicateKeyException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.DATABASE_DUPLICATE_KEY, "Duplicate key", null)
+ }
+
+ is NoRecordFoundException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.DATABASE_NO_RECORD_FOUND, e.localizedMessage, null)
+ }
+
+ is UncategorizedSQLException -> {
+ if (e.localizedMessage.contains("SQLITE_CONSTRAINT_UNIQUE")) {
+ logger.debug(e.localizedMessage, e)
+ return ResponseResult.fail(ResponseCode.DATABASE_DUPLICATE_KEY, "Duplicate key", null)
+ }
+
+ logger.error(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, e.localizedMessage, null)
+ }
+
+ is RecordAlreadyExists -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.DATABASE_RECORD_ALREADY_EXISTS, e.localizedMessage, null)
+ }
+
+ /* Tool */
+ is IllegalVersionException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_ILLEGAL_VERSION, e.localizedMessage, null)
+ }
+
+ is ToolUnderReviewException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_UNDER_REVIEW, e.localizedMessage, null)
+ }
+
+ is ToolNotUnderReviewException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_NOT_UNDER_REVIEW, e.localizedMessage, null)
+ }
+
+ is ToolHasUnpublishedVersionException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_HAS_UNPUBLISHED_VERSION, e.localizedMessage, null)
+ }
+
+ is ToolHasNotBeenPublishedException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_HAS_NOT_BEEN_PUBLISHED, e.localizedMessage, null)
+ }
+
+ is ToolHasBeenPublishedException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.TOOL_HAS_BEEN_PUBLISHED, e.localizedMessage, null)
+ }
+
+ /* Other */
+ is MatchSensitiveWordException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_MATCH_SENSITIVE_WORD, e.localizedMessage, null)
+ }
+
+ /* API */
+ is AvatarException -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.API_AVATAR_ERROR, e.localizedMessage, null)
+ }
+
+ else -> {
+ logger.error(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_ERROR, e.toString(), null)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAccessDeniedHandler.kt b/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAccessDeniedHandler.kt
new file mode 100644
index 0000000..294b888
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAccessDeniedHandler.kt
@@ -0,0 +1,23 @@
+package top.fatweb.oxygen.api.handler
+
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.web.access.AccessDeniedHandler
+import org.springframework.stereotype.Component
+
+/**
+ * Jwt access denied handler
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AccessDeniedHandler
+ */
+@Component
+class JwtAccessDeniedHandler : AccessDeniedHandler {
+ override fun handle(
+ request: HttpServletRequest?, response: HttpServletResponse?, accessDeniedException: AccessDeniedException?
+ ) {
+ throw accessDeniedException ?: AccessDeniedException("Access Denied")
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAuthenticationEntryPointHandler.kt b/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAuthenticationEntryPointHandler.kt
new file mode 100644
index 0000000..b215232
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/JwtAuthenticationEntryPointHandler.kt
@@ -0,0 +1,24 @@
+package top.fatweb.oxygen.api.handler
+
+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
+
+/**
+ * Jwt authentication entry point handler
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see AuthenticationEntryPoint
+ */
+@Component
+class JwtAuthenticationEntryPointHandler : AuthenticationEntryPoint {
+ override fun commence(
+ request: HttpServletRequest?, response: HttpServletResponse?, authException: AuthenticationException?
+ ) {
+ request?.setAttribute("filter.error", authException)
+ request?.getRequestDispatcher("/error/thrown")?.forward(request, response)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt b/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt
new file mode 100644
index 0000000..7f28258
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt
@@ -0,0 +1,33 @@
+package top.fatweb.oxygen.api.http
+
+import com.github.lianjiatech.retrofit.spring.boot.core.RetrofitClient
+import org.springframework.stereotype.Service
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.POST
+import top.fatweb.oxygen.api.http.entity.turnstile.SiteverifyResponse
+import top.fatweb.oxygen.api.properties.ServerProperties
+
+/**
+ * Turnstile http request api
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Service
+@RetrofitClient(baseUrl = "https://challenges.cloudflare.com/turnstile/v0/")
+interface TurnstileApi {
+ /**
+ * Turnstile post verify captcha code
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see SiteverifyResponse
+ */
+ @FormUrlEncoded
+ @POST("siteverify")
+ fun siteverify(
+ @Field("response") captchaCode: String,
+ @Field("secret") secret: String = ServerProperties.turnstileSecretKey
+ ): SiteverifyResponse
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt b/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt
new file mode 100644
index 0000000..e6e6b94
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt
@@ -0,0 +1,48 @@
+package top.fatweb.oxygen.api.http.entity.turnstile
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.time.LocalDateTime
+
+/**
+ * Turnstile verify captcha code response
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+data class SiteverifyResponse(
+ /**
+ * Is success
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonProperty("success")
+ val success: Boolean,
+
+ /**
+ * Challenge time
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonProperty("challenge_ts")
+ val challengeTs: LocalDateTime?,
+
+ /**
+ * Hostname
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonProperty("hostname")
+ val hostname: String?,
+
+ /**
+ * Error codes list
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @JsonProperty("error-codes")
+ val errorCodes: List?
+)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/FuncMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/FuncMapper.kt
new file mode 100644
index 0000000..167353d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/FuncMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.Func
+
+/**
+ * Function mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Func
+ */
+@Mapper
+interface FuncMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/GroupMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/GroupMapper.kt
new file mode 100644
index 0000000..8413f65
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/GroupMapper.kt
@@ -0,0 +1,57 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.permission.Group
+
+/**
+ * Group mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Group
+ */
+@Mapper
+interface GroupMapper : BaseMapper {
+ /**
+ * Select group in page
+ *
+ * @param page Pagination
+ * @param searchName Name to search for
+ * @param searchRegex Use regex
+ * @return Group in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectPage(
+ page: IPage,
+ @Param("searchName") searchName: String?,
+ @Param("searchRegex") searchRegex: Boolean
+ ): IPage
+
+ /**
+ * Select group with role list by list of group IDs
+ *
+ * @param groupIds List of group IDs
+ * @return Group with role list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Group
+ */
+ fun selectListWithRoleByIds(@Param("groupIds") groupIds: List): List?
+
+ /**
+ * Select one group by ID
+ *
+ * @param id Group ID
+ * @return Group object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Group
+ */
+ fun selectOneById(@Param("id") id: Long): Group?
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/MenuMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/MenuMapper.kt
new file mode 100644
index 0000000..1952dcd
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/MenuMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.Menu
+
+/**
+ * Menu mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Menu
+ */
+@Mapper
+interface MenuMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/ModuleMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/ModuleMapper.kt
new file mode 100644
index 0000000..bc2e492
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/ModuleMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.Module
+
+/**
+ * Module mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Module
+ */
+@Mapper
+interface ModuleMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/OperationMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/OperationMapper.kt
new file mode 100644
index 0000000..848a5bf
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/OperationMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.Operation
+
+/**
+ * Operation mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Operation
+ */
+@Mapper
+interface OperationMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/PowerMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/PowerMapper.kt
new file mode 100644
index 0000000..1100fdd
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/PowerMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.Power
+
+/**
+ * Power mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Power
+ */
+@Mapper
+interface PowerMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RPowerRoleMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RPowerRoleMapper.kt
new file mode 100644
index 0000000..be569f3
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RPowerRoleMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.RPowerRole
+
+/**
+ * Power role intermediate mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see RPowerRole
+ */
+@Mapper
+interface RPowerRoleMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RRoleGroupMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RRoleGroupMapper.kt
new file mode 100644
index 0000000..48f10da
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RRoleGroupMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.RRoleGroup
+
+/**
+ * Role group intermediate mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see RRoleGroup
+ */
+@Mapper
+interface RRoleGroupMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserGroupMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserGroupMapper.kt
new file mode 100644
index 0000000..fa0b8bb
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserGroupMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.RUserGroup
+
+/**
+ * User group intermediate mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see RUserGroup
+ */
+@Mapper
+interface RUserGroupMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserRoleMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserRoleMapper.kt
new file mode 100644
index 0000000..b6a18f4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RUserRoleMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.RUserRole
+
+/**
+ * User role intermediate mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see RUserRole
+ */
+@Mapper
+interface RUserRoleMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RoleMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RoleMapper.kt
new file mode 100644
index 0000000..b036bee
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/RoleMapper.kt
@@ -0,0 +1,57 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.permission.Role
+
+/**
+ * Role mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Role
+ */
+@Mapper
+interface RoleMapper : BaseMapper {
+ /**
+ * Select role in page
+ *
+ * @param page Pagination
+ * @param searchName Name to search for
+ * @param searchRegex Use regex
+ * @return Role in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectPage(
+ page: IPage,
+ @Param("searchName") searchName: String?,
+ @Param("searchRegex") searchRegex: Boolean
+ ): IPage
+
+ /**
+ * Select role with power list by list of role IDs
+ *
+ * @param roleIds List of role IDs
+ * @return Role with power list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ */
+ fun selectListWithPowerByIds(@Param("roleIds") roleIds: List): List?
+
+ /**
+ * Select one role by ID
+ *
+ * @param id Role ID
+ * @return Role object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Role
+ */
+ fun selectOneById(@Param("id") id: Long): Role?
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserInfoMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserInfoMapper.kt
new file mode 100644
index 0000000..8e816d6
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserInfoMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.permission.UserInfo
+
+/**
+ * User information mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see UserInfo
+ */
+@Mapper
+interface UserInfoMapper : BaseMapper
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserMapper.kt
new file mode 100644
index 0000000..80c1aad
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/permission/UserMapper.kt
@@ -0,0 +1,111 @@
+package top.fatweb.oxygen.api.mapper.permission
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.permission.User
+
+/**
+ * User mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see User
+ */
+@Mapper
+interface UserMapper : BaseMapper {
+ /**
+ * Select one user with power and information by username or email
+ *
+ * @param account Username or email
+ * @return User object with power and information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ fun selectOneWithPowerInfoByAccount(@Param("account") account: String): User?
+
+ /**
+ * Select one user with basic information by username
+ *
+ * @param username Username
+ * @return User object with basic information
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ fun selectOneWithBasicInfoByUsername(@Param("username") username: String): User?
+
+ /**
+ * Select user ID in page
+ *
+ * @param page Pagination
+ * @param searchType Type of search
+ * @param searchValue Value to search for
+ * @param searchRegex Use regex
+ * @return User in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectPage(
+ page: IPage,
+ @Param("searchType") searchType: String,
+ @Param("searchValue") searchValue: String?,
+ @Param("searchRegex") searchRegex: Boolean
+ ): IPage
+
+ /**
+ * Select user with role and information list by list of user IDs
+ *
+ * @param userIds List of user IDs
+ * @return User with role and information list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ fun selectListWithRoleInfoByIds(@Param("userIds") userIds: List): List
+
+ /**
+ * Select one user by ID
+ *
+ * @param id User ID
+ * @return User object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ fun selectOneWithRoleInfoById(@Param("id") id: Long): User?
+
+ /**
+ * Select all user with information list
+ *
+ * @return User with information list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see User
+ */
+ fun selectListWithInfo(): List
+
+ /**
+ * Select user IDs list by list of role IDs
+ *
+ * @param roleIds List of role IDs
+ * @return User IDs list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun selectIdsWithRoleIds(@Param("roleIds") roleIds: List): List
+
+ /**
+ * Select user IDs list by list of group IDs
+ *
+ * @param groupIds List of group IDs
+ * @return User IDs list
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun selectIdsWithGroupIds(@Param("groupIds") groupIds: List): List
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/EventLogMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/EventLogMapper.kt
new file mode 100644
index 0000000..aaf96e0
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/EventLogMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.system
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.system.EventLog
+
+/**
+ * Event log mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see EventLog
+ */
+@Mapper
+interface EventLogMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SensitiveWordMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SensitiveWordMapper.kt
new file mode 100644
index 0000000..be01e0c
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SensitiveWordMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.system
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.system.SensitiveWord
+
+/**
+ * Sensitive word mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see SensitiveWord
+ */
+@Mapper
+interface SensitiveWordMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/StatisticsLogMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/StatisticsLogMapper.kt
new file mode 100644
index 0000000..59b1523
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/StatisticsLogMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.system
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.system.StatisticsLog
+
+/**
+ * Statistics log mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see StatisticsLog
+ */
+@Mapper
+interface StatisticsLogMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SysLogMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SysLogMapper.kt
new file mode 100644
index 0000000..06088a2
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/system/SysLogMapper.kt
@@ -0,0 +1,44 @@
+package top.fatweb.oxygen.api.mapper.system
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.system.SysLog
+import java.time.LocalDateTime
+
+/**
+ * System log mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see SysLog
+ */
+@Mapper
+interface SysLogMapper : BaseMapper {
+ /**
+ * Select system log in page
+ *
+ * @param page Pagination
+ * @param logType List of log types
+ * @param requestMethod List of request methods
+ * @param searchRequestUrl Request URL to search for
+ * @param searchStartTime Start time to search for
+ * @param searchEndTime end time to search for
+ * @return System log in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ * @see SysLog
+ * @see LocalDateTime
+ */
+ fun selectPage(
+ page: IPage,
+ @Param("logType") logType: List?,
+ @Param("requestMethod") requestMethod: List?,
+ @Param("searchRequestUrl") searchRequestUrl: String?,
+ @Param("searchStartTime") searchStartTime: LocalDateTime?,
+ @Param("searchEndTime") searchEndTime: LocalDateTime?
+ ): IPage
+}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/EditMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/EditMapper.kt
new file mode 100644
index 0000000..5f6bbcc
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/EditMapper.kt
@@ -0,0 +1,88 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.tool.Platform
+import top.fatweb.oxygen.api.entity.tool.Tool
+import top.fatweb.oxygen.api.entity.tool.ToolTemplate
+
+/**
+ * Tool edit mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Tool
+ */
+@Mapper
+interface EditMapper : BaseMapper {
+ /**
+ * Select tool template by ID
+ *
+ * @param id Template ID
+ * @return ToolTemplate object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplate
+ */
+ fun selectTemplate(@Param("id") id: Long): ToolTemplate?
+
+ /**
+ * Select tool by ID and user ID
+ *
+ * @param id Tool ID
+ * @param userId User ID
+ * @return Tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ */
+ fun selectOne(@Param("id") id: Long, @Param("userId") userId: Long): Tool?
+
+ /**
+ * Select tool ID by user ID in page
+ *
+ * @param page Pagination
+ * @param userId User ID
+ * @return Tool ID in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectPersonalToolIdPage(page: IPage, @Param("userId") userId: Long): IPage
+
+ /**
+ * Select tool in list by tool IDs and user ID
+ *
+ * @param toolIds List of tool Ids
+ * @param userId User ID
+ * @return List of Tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ */
+ fun selectListByToolIds(@Param("toolIds") toolIds: List, @Param("userId") userId: Long): List
+
+ /**
+ * Select tool detail
+ *
+ * @param username Username
+ * @param toolId Tool ID
+ * @param ver Tool version
+ * @param operator Operator username
+ * @return List of tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Platform
+ * @see Tool
+ */
+ fun selectDetail(
+ @Param("username") username: String,
+ @Param("toolId") toolId: String,
+ @Param("ver") ver: String,
+ @Param("platform") platform: Platform,
+ @Param("operator") operator: String?
+ ): Tool?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ManagementMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ManagementMapper.kt
new file mode 100644
index 0000000..0ef1619
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ManagementMapper.kt
@@ -0,0 +1,62 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.tool.Tool
+
+/**
+ * Tool management mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Tool
+ */
+@Mapper
+interface ManagementMapper : BaseMapper {
+ /**
+ * Select tool by ID
+ *
+ * @param id Tool ID
+ * @return Tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ */
+ fun selectOne(@Param("id") id: Long): Tool?
+
+ /**
+ * Select tool ID in page
+ *
+ * @param page Pagination
+ * @param review Review
+ * @param searchType Type of search
+ * @param searchValue Value to search for
+ * @param searchRegex Use regex
+ * @return Tool ID in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectPage(
+ page: IPage,
+ @Param("review") review: List?,
+ @Param("platform") platform: List?,
+ @Param("searchType") searchType: String,
+ @Param("searchValue") searchValue: String?,
+ @Param("searchRegex") searchRegex: Boolean
+ ): IPage
+
+ /**
+ * Select tool in list by tool IDs
+ *
+ * @param ids List of tool IDs
+ * @return List of tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ */
+ fun selectListByIds(@Param("ids") ids: List): List
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/RToolCategoryMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/RToolCategoryMapper.kt
new file mode 100644
index 0000000..1bc8be4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/RToolCategoryMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.tool.RToolCategory
+
+/**
+ * Tool category intermediate mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see RToolCategory
+ */
+@Mapper
+interface RToolCategoryMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/StoreMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/StoreMapper.kt
new file mode 100644
index 0000000..4d3a11d
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/StoreMapper.kt
@@ -0,0 +1,73 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.tool.Platform
+import top.fatweb.oxygen.api.entity.tool.Tool
+
+/**
+ * Tool store mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see Tool
+ */
+@Mapper
+interface StoreMapper : BaseMapper {
+ /**
+ * Select author and tool ID in page
+ *
+ * @param page Pagination
+ * @param searchValue Value to search for
+ * @return Author:Tool_ID in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectAuthorToolIdPage(
+ page: IPage,
+ @Param("searchValue") searchValue: String?,
+ @Param("platform") platform: Platform? = null
+ ): IPage
+
+ /**
+ * Select author and tool ID by username in page
+ *
+ * @param page Pagination
+ * @param username Username
+ * @return Author:Tool_ID in page
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see IPage
+ */
+ fun selectAuthorToolIdPageByUsername(page: IPage, @Param("username") username: String): IPage
+
+ /**
+ * Select tool in list by Author:Tool_ID
+ *
+ * @param ids List of Author:Tool_ID
+ * @return List of tool object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see Tool
+ */
+ fun selectListByAuthorToolIds(
+ @Param("ids") ids: List,
+ @Param("operator") operator: Long?,
+ @Param("platform") platform: Platform? = null
+ ): List
+
+ /**
+ * Count published tool by username and toolId
+ *
+ * @param authorId Author ID
+ * @param toolId Tool ID
+ * @return Number
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ fun countPublishedToolByAuthorAndToolId(@Param("authorId") authorId: Long, @Param("toolId") toolId: String): Long
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolBaseMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolBaseMapper.kt
new file mode 100644
index 0000000..26f470f
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolBaseMapper.kt
@@ -0,0 +1,28 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.tool.ToolBase
+
+/**
+ * Tool base mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see ToolBase
+ */
+@Mapper
+interface ToolBaseMapper : BaseMapper {
+ /**
+ * Select tool base by ID
+ *
+ * @param id Tool base ID
+ * @return ToolBase object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolBase
+ */
+ fun selectOne(@Param("id") id: Long): ToolBase?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolCategoryMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolCategoryMapper.kt
new file mode 100644
index 0000000..414eb72
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolCategoryMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.tool.ToolCategory
+
+/**
+ * Tool category mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see ToolCategory
+ */
+@Mapper
+interface ToolCategoryMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolDataMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolDataMapper.kt
new file mode 100644
index 0000000..088f219
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolDataMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.tool.ToolData
+
+/**
+ * Tool data mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see ToolData
+ */
+@Mapper
+interface ToolDataMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolFavoriteMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolFavoriteMapper.kt
new file mode 100644
index 0000000..213975b
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolFavoriteMapper.kt
@@ -0,0 +1,16 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import org.apache.ibatis.annotations.Mapper
+import top.fatweb.oxygen.api.entity.tool.ToolFavorite
+
+/**
+ * Tool favorite mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see ToolFavorite
+ */
+@Mapper
+interface ToolFavoriteMapper : BaseMapper
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolTemplateMapper.kt b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolTemplateMapper.kt
new file mode 100644
index 0000000..d009b4c
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/mapper/tool/ToolTemplateMapper.kt
@@ -0,0 +1,42 @@
+package top.fatweb.oxygen.api.mapper.tool
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.baomidou.mybatisplus.core.metadata.IPage
+import org.apache.ibatis.annotations.Mapper
+import org.apache.ibatis.annotations.Param
+import top.fatweb.oxygen.api.entity.tool.ToolTemplate
+
+/**
+ * Tool template mapper
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see BaseMapper
+ * @see ToolTemplate
+ */
+@Mapper
+interface ToolTemplateMapper : BaseMapper {
+ /**
+ * Select tool template by ID
+ *
+ * @param id Tool template ID
+ * @return ToolTemplate object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplate
+ */
+ fun selectOne(@Param("id") id: Long): ToolTemplate?
+
+ /**
+ * Select tool template in list
+ *
+ * @return List of ToolTemplate object
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ * @see ToolTemplate
+ */
+ fun selectListWithBaseName(
+ page: IPage,
+ @Param("platform") platform: List?
+ ): IPage
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt
new file mode 100644
index 0000000..876e665
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt
@@ -0,0 +1,25 @@
+package top.fatweb.oxygen.api.param
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.NotBlank
+import top.fatweb.oxygen.api.annotation.Trim
+
+/**
+ * Captcha code parameter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+@Trim
+open class CaptchaCodeParam {
+ /**
+ * Captcha code
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Trim
+ @Schema(description = "验证码", required = true)
+ @field:NotBlank(message = "Captcha code can not be blank")
+ var captchaCode: String? = null
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/PageSortParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/PageSortParam.kt
new file mode 100644
index 0000000..cab7e95
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/PageSortParam.kt
@@ -0,0 +1,50 @@
+package top.fatweb.oxygen.api.param
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.Min
+
+/**
+ * Page sort parameters
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+open class PageSortParam {
+ /**
+ * Current page number
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "分页页码", defaultValue = "1", example = "1")
+ @field:Min(1, message = "Pagination page number must be a positive integer")
+ var currentPage: Long = 1
+
+ /**
+ * Size of page
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "分页大小", defaultValue = "20", example = "20")
+ @field:Min(1, message = "The number of data per page must be a positive integer")
+ var pageSize: Long = 20
+
+ /**
+ * Field name to sort
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "排序字段", example = "id")
+ var sortField: String? = null
+
+ /**
+ * Sort order by
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "排序方式", allowableValues = ["desc", "asc"], defaultValue = "desc", example = "desc")
+ var sortOrder: String? = null
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/api/v1/avatar/AvatarBaseParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/api/v1/avatar/AvatarBaseParam.kt
new file mode 100644
index 0000000..c949017
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/api/v1/avatar/AvatarBaseParam.kt
@@ -0,0 +1,69 @@
+package top.fatweb.oxygen.api.param.api.v1.avatar
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Pattern
+
+/**
+ * Avatar base parameters
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+open class AvatarBaseParam {
+ /**
+ * Seed to generate avatar
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "种子")
+ var seed: Long? = null
+
+ /**
+ * Size of image
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "图像大小", defaultValue = "128")
+ @field:Max(256, message = "Size must be less than or equal to 256")
+ var size: Int? = null
+
+ /**
+ * Margin of image
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "外边距", defaultValue = "0")
+ var margin: Int? = null
+
+ /**
+ * Padding of image
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "内边距", defaultValue = "0")
+ var padding: Int? = null
+
+ /**
+ * List of colors to generate avatar
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "颜色列表", example = "#FFFFFFAA")
+ var colors: List